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:
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { test, expect } from '../../fixtures/test';
|
||||
import { mintSseTicket } from '../../helpers/sse';
|
||||
import { seedUpload } from '../../helpers/seed';
|
||||
|
||||
const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101';
|
||||
|
||||
@@ -30,19 +31,50 @@ test.describe('Adversarial — small-scale abuse', () => {
|
||||
expect(statuses.some((s) => s === 201 || s === 409)).toBe(true);
|
||||
});
|
||||
|
||||
test('10 MB comment body is rejected (multipart-less endpoint)', async ({ guest }) => {
|
||||
const g = await guest('BigComment');
|
||||
const huge = 'A'.repeat(10 * 1024 * 1024);
|
||||
const res = await fetch(`${BASE}/api/v1/upload/00000000-0000-0000-0000-000000000000/comments`, {
|
||||
// The comment body cap lives in [backend/src/handlers/social.rs] `add_comment`:
|
||||
// if text_chars == 0 || text_chars > 500 → 400
|
||||
// It must be exercised against a REAL upload: the handler looks the upload up (and
|
||||
// 404s) *before* it reaches the length check, so posting to a non-existent id proves
|
||||
// nothing about the cap.
|
||||
async function postComment(jwt: string, uploadId: string, body: string) {
|
||||
return fetch(`${BASE}/api/v1/upload/${uploadId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${g.jwt}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ body: huge }),
|
||||
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ body }),
|
||||
});
|
||||
// 400 (length cap), 404 (no such upload), 413 (payload too large), 429 (rate-limited),
|
||||
// or 502 (Caddy rejected the body before it reached the backend) — all fine.
|
||||
expect([400, 404, 413, 429, 502]).toContain(res.status);
|
||||
// Not 200 — that would mean we accepted a 10 MB comment.
|
||||
expect(res.status).not.toBe(200);
|
||||
}
|
||||
|
||||
test('comment body over the 500-char cap is rejected with 400', async ({ guest }) => {
|
||||
const g = await guest('LongComment');
|
||||
const uploadId = await seedUpload(g.jwt);
|
||||
|
||||
const res = await postComment(g.jwt, uploadId, 'A'.repeat(501));
|
||||
expect(res.status, '501 chars must be rejected by the length cap').toBe(400);
|
||||
const json: any = await res.json().catch(() => ({}));
|
||||
expect((json.message ?? '').toLowerCase()).toMatch(/500 zeichen/);
|
||||
});
|
||||
|
||||
test('comment body exactly at the 500-char cap is accepted', async ({ guest }) => {
|
||||
const g = await guest('MaxComment');
|
||||
const uploadId = await seedUpload(g.jwt);
|
||||
|
||||
// The boundary must be inclusive — otherwise the "cap" is really 499 and the
|
||||
// rejection test above would also pass on an off-by-one implementation.
|
||||
const res = await postComment(g.jwt, uploadId, 'A'.repeat(500));
|
||||
expect(res.status, '500 chars is the documented maximum and must be accepted').toBe(201);
|
||||
});
|
||||
|
||||
test('10 MB comment body never reaches the handler (body-size limit rejects it)', async ({ guest }) => {
|
||||
const g = await guest('BigComment');
|
||||
const uploadId = await seedUpload(g.jwt);
|
||||
const huge = 'A'.repeat(10 * 1024 * 1024);
|
||||
|
||||
const res = await postComment(g.jwt, uploadId, huge);
|
||||
// This asserts ONLY what it can prove: a 10 MB JSON body is refused somewhere on the
|
||||
// path (Caddy's request-body limit → 502/413, or the backend's own body limit → 413,
|
||||
// or the 500-char cap if it does get through → 400). The upload exists, so a 404 here
|
||||
// would be a bug, and a 201 would mean we stored a 10 MB comment.
|
||||
expect([400, 413, 502]).toContain(res.status);
|
||||
});
|
||||
|
||||
test('SSE: 10 concurrent streams from one user do not crash the server', async ({ guest }) => {
|
||||
|
||||
Reference in New Issue
Block a user