test(e2e): de-vacuum security tests; add quota, authz-sweep, keepsake-regen coverage

An audit found tests that pass on broken code. The dominant pattern: fire a security
assertion at the all-zeros UUID and accept [403, 404] — the 404 comes from the resource
LOOKUP, not the guard, so the guard can be deleted and the test still passes. Repaired to
use real resources and demand exactly 403:
  - banned-user cannot like / comment (the only coverage of those ban invariants)
  - host cannot promote a real guest (or self) to admin — asserts nobody's role changed
  - IDOR comment-delete already used a real resource; kept
Also inverted recovery.spec's "unknown name → nicht gefunden" test: that asserted the exact
account-enumeration oracle the F4 fix removed, so restoring the vuln would have made it
pass. Now: an unknown name must be byte-identical to a wrong PIN.

New coverage for paths that ran in production but in zero tests:
  - quota.spec.ts: storage quota enforcement (413 over-limit, atomic increment under two
    uploads held mid-body so both carry a stale total=0 — the real race; a naive Promise.all
    version was itself vacuous and is documented as such). Proven to fail without the guard.
  - authz-sweep.spec.ts: table-driven guest→403 / host→403 over ALL 19 privileged routes +
    anonymous + __truncate. No live hole found; the whole surface is now locked.
  - ban / unban / host-comment-delete AFTER release regenerate the keepsake (data-loss
    paths that were dead under test); comment-delete mid-build doesn't strand the ZIP.

Lower-severity de-vacuuming: 10 MB comment test hit Caddy's 502 before the real 500-char
cap (now seeds a real upload, 501→400 / 500→201, mutation-verified); XSS name payloads
shortened under the 50-char cap so they actually store+render; ui-rendering XSS test now
proves the payload rendered before asserting no <b>; export page-object locators fixed to
the real "Download" label with a positive empty-state anchor; avatar palette spread test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-15 07:26:40 +02:00
parent e5201a9889
commit 02971f3186
13 changed files with 681 additions and 67 deletions

View File

@@ -26,12 +26,39 @@ test.describe('Auth — /recover route', () => {
await expect(recover.errorMessage).toContainText(/PIN ist falsch|falsch/i);
});
test('non-existent name returns the "not found" error', async ({ page }) => {
// An unknown name must be INDISTINGUISHABLE from a wrong PIN — same status, same message.
//
// This test used to assert the exact opposite: that an unknown name produced a distinct
// "nicht gefunden" error. That IS the account-enumeration oracle the F4 audit fix removed
// (recover now answers an unknown name with the same 401 "PIN ist falsch."), so reintroducing
// the vulnerability would have made this test pass and `recover-enumeration.spec.ts` fail — two
// tests asserting contradictory things about the same behaviour, one of them for the attacker.
//
// It also passed for a reason unrelated to the name: this test takes no `guest` fixture, so the
// per-test TRUNCATE leaves no event row at all, and recover 404s with "Event nicht gefunden." —
// which the old regex happily matched.
test('an unknown name is indistinguishable from a wrong PIN (no enumeration oracle)', async ({
page,
guest,
}) => {
const handle = await guest('Known');
await clearAllStorage(page);
const recover = new RecoverPage(page);
await recover.goto();
await recover.recover('Doesnt-Exist-' + Date.now(), '1234');
await expect(recover.errorMessage).toContainText(/nicht gefunden|kein/i);
const unknownNameError = (await recover.errorMessage.textContent())?.trim();
await recover.goto();
await recover.recover('Known', handle.pin === '9999' ? '0000' : '9999');
const wrongPinError = (await recover.errorMessage.textContent())?.trim();
expect(unknownNameError).toBeTruthy();
expect(
unknownNameError,
'an unknown name must not be distinguishable from a wrong PIN — that is an account-enumeration oracle'
).toBe(wrongPinError);
expect(unknownNameError).not.toMatch(/nicht gefunden|kein benutzer/i);
});
test('lockout expires and counter resets (per recover handler)', async ({ api, guest, db }) => {