test(e2e): pin unauthenticated wget-style denial for copyparty and homepage
Wget-style (no-JS) clients must never receive application bytes from guarded services: copyparty serves authenticated traffic with anonymous read perms, so the guard chain is the only boundary between an unauthenticated client and the file bytes. Assertions target leakage markers in app code (Copyparty UI title, homepage panel grid, services.json JSON shape), not gate internals, so any guard-route regression fails the suite while any gate implementation passes. Also route 15-*.spec.ts into the guest project (empty storage state).
This commit is contained in:
@@ -36,7 +36,7 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
name: "guest",
|
||||
testMatch: /14-.*\.spec\.ts/,
|
||||
testMatch: /(14|15)-.*\.spec\.ts/,
|
||||
use: { storageState: { cookies: [], origins: [] } },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// Wget-style denial: a non-browser client (curl/wget/WebDAV clients solve no
|
||||
// JS challenge) must never receive application bytes from a guarded service.
|
||||
// Today the guard chain answers every unauthenticated path — roots and deep
|
||||
// paths alike — with an HTML gate page, so a status-code-only assertion would
|
||||
// pass even if a route leaked: these tests assert on LEAKAGE instead. The
|
||||
// markers below live in APP code (copyparty's UI title, the homepage panel
|
||||
// grid, the registry JSON shape), so any guard-route regression that lets the
|
||||
// app answer directly fails here, while any gate implementation (challenge or
|
||||
// login flow) passes.
|
||||
//
|
||||
// Copyparty serves authenticated traffic with anonymous read perms (`r` for
|
||||
// all authenticated traffic; see dankserver/copyparty/quadlet), so the guard
|
||||
// chain is the ONLY thing between an unauthenticated client and the file
|
||||
// bytes — if any route bypasses the guard, copyparty answers with a real
|
||||
// directory listing and these tests fail.
|
||||
|
||||
const FILES = "https://files.franksdank.cloud";
|
||||
const BASE = "https://www.franksdank.cloud";
|
||||
|
||||
// Paths probed as an unauthenticated raw client. These are probes, not claims
|
||||
// that the path exists when authenticated: whatever lives behind a path, the
|
||||
// answer to an unauthenticated raw client must be the gate — never app bytes.
|
||||
const FILE_PATHS = ["/", "/w/", "/cfg/", "/dank/", "/favicon.ico", "/robots.txt"];
|
||||
|
||||
test("wget-style GETs of copyparty paths never return file bytes to unauthenticated clients", async ({ request }) => {
|
||||
for (const p of FILE_PATHS) {
|
||||
const resp = await request.get(`${FILES}${p}`, { timeout: 30_000 });
|
||||
const body = await resp.text();
|
||||
// The gate answers in HTML; a copyparty file/listing response would not.
|
||||
expect(
|
||||
resp.headers()["content-type"] ?? "",
|
||||
`${p}: content-type must stay text/html while unauthenticated`,
|
||||
).toMatch(/text\/html/);
|
||||
// No leakage: copyparty's UI markers must not appear anywhere in the body.
|
||||
expect(body, `${p}: copyparty bytes leaked to an unauthenticated client`).not.toMatch(/copyparty/i);
|
||||
}
|
||||
});
|
||||
|
||||
test("homepage app bytes are never served to unauthenticated clients", async ({ request }) => {
|
||||
// The homepage HTML itself: an unauthenticated GET must render the gate,
|
||||
// not the panel grid.
|
||||
const root = await request.get(`${BASE}/`, { timeout: 30_000 });
|
||||
expect(await root.headers()).toBeTruthy();
|
||||
expect((await root.text()), "homepage app markup leaked to an unauthenticated client").not.toContain('id="grid"');
|
||||
|
||||
// The services registry JSON: the inventory of internal services must never
|
||||
// be served pre-auth — not even as a "harmless" static file.
|
||||
const registry = await request.get(`${BASE}/services.json`, { timeout: 30_000 });
|
||||
const ctype = (registry.headers()["content-type"] ?? "") as string;
|
||||
expect(ctype, "services.json must not be served as JSON to unauthenticated clients").not.toMatch(/application\/json/);
|
||||
expect(await registry.text(), "services registry leaked to an unauthenticated client").not.toContain('"category"');
|
||||
|
||||
// Admin panel: the app's own RBAC denial page is itself app output — even
|
||||
// that must not reach an unauthenticated client; the gate answers first.
|
||||
const admin = await request.get(`${BASE}/admin/`, { timeout: 30_000 });
|
||||
expect(
|
||||
await admin.text(),
|
||||
"the homepage admin panel answered before the guard chain did",
|
||||
).not.toContain("homepage admins only");
|
||||
});
|
||||
Reference in New Issue
Block a user