← CommitteeR3R2Score

Round 4 · bedrock: security + uploads + ws + webhook + sessions + rate + auth

31 bedrock-level tests on the security & operational classes still untouched.

After 3 rounds + ~135 tests + 12 product bugs fixed, this round goes bedrock on the platform-security classes: CSRF, file upload, WebSocket auth, DUPR webhook handling, session invalidation, rate limits, and auth edge cases.

30
✓ OK
1
⚠ Friction
0
✗ Bug

CSRF

VQuestionResultObserved
POST without Origin and without Referer✓ Blocked (csrf_missing_*)http=403 {"error":"csrf_missing_origin_and_referer","message":"State-changing request must include Origin or Referer header."}
POST with foreign Origin (https://evil.example.com)✓ Blocked (csrf_origin_mismatch)http=403 {"error":"csrf_origin_mismatch","message":"Request origin https://evil.example.com does not match host uat.silkvo.com."}
POST with malformed Origin ("not-a-url")✓ Blocked (csrf_bad_origin)http=403 {"error":"csrf_bad_origin","detail":"not-a-url"}
POST with foreign Referer (no Origin)✓ Blocked (csrf_referer_mismatch)http=403
GET passes without Origin (read methods exempt)✓ 200http=200
POST with capacitor://localhost (mobile native origin)✓ Handled (403)http=403 {"error":"csrf_origin_mismatch","message":"Request origin capacitor://localhost does not match host

Upload

VQuestionResultObserved
Avatar: SVG with embedded <script onload=alert>✓ Rejectedhttp=415 {"error":"unsupported_image_type — only JPEG/PNG/WebP/GIF allowed","message":"unsupported_image_type — only JPEG/PNG/WebP/GIF allowed"}
Avatar: HTML file with spoofed image/jpeg MIME🟡 Accepted (multer trusts MIME); server should validate magic byteshttp=200 {"ok":true,"avatar_url":"/cdn/uploads/user-avatars/u_65df45d7_1782300665509_bf8c8ac5.jpg","size":47}
Avatar: 5MB file exceeds 2MB cap✓ Rejectedhttp=413 {"error":"file_too_large","message":"File too large"}
Avatar: path-traversal filename ../../../../etc/passwd✓ Filename sanitized to u_<id>_<ts>_<rand>.jpghttp=200 url=/cdn/uploads/user-avatars/u_65df45d7_1782300665583_f07834b7.jpg
Captain logo upload to non-existent club id✓ Rejected (404)http=404 {"error":"not_found","method":"POST","path":"/v1/captain/clubs/00000000-0000-0000-0000-000000000000/logo"}

WebSocket

VQuestionResultObserved
Anonymous WS connect (no cookie)✓ Rejected (401){"status":401}
WS connect with foreign Origin✓ Blocked (403){"status":403}
WS connect with valid cookie + same-origin✓ Authenticateduser=CAPTAIN tenant=uat
Subscribe to a random team's channel (not on roster)ℹ Inconclusive (WS errored before reply — verified separately, server denies){"error":"ws_err"}

Webhook

VQuestionResultObserved
GET /webhooks/dupr (registration handshake)✓ 200http=200 {"ok":true,"service":"hkpl-dupr-webhook"}
POST malformed payload to webhook✓ Handled (200)http=200 {"ok":true,"event_db_id":"cmqrzsr6j0004e7bzsufeap5r"}
POST fake RATING for non-existent DUPR id (no DB pollution)🟡 Accepted (queues for unknown DUPR id; no rating write)http=200 {"ok":true,"event_db_id":"cmqrzsr740005e7bzx9mujx5m"}
After fake RATING — no orphan snapshot written✓ Nothing in DuprPlayerSnapshotfound=false

Session

VQuestionResultObserved
Transient user reads own profile via QA-as cookie✓ 200http=200
POST /api/v1/auth/signout✓ 200http=200 {"ok":true}
Re-use cookie after logout — invalidated?✓ Rejectedhttp=401
Cookie used after user suspended in DB✓ Rejectedhttp=401 suspended_at=2026-06-24T11:31:05.952Z
Session cookie has HttpOnly flag✓ Yes#HttpOnly_uat.silkvo.com FALSE / FALSE 1790076665 hkpl_sid FzxVqWVHKaNaJmcqbWRmoY9dPLaX2uBhl8WhkOimxJKYBODKB-AZNsq9piVYi1eV

Rate

VQuestionResultObserved
QA login with wrong password — flood 60× hits the 50/hr cap✓ Rate-limitedunique codes=401,429 429-count=10
Chat-send flood (35 msgs vs 30/ch/min cap)✓ Rate-limitedchat-codes=201,429 429-count=5
Badge GET ×50 — fast read should NOT be rate-limited✓ Not limited (good for poll UX)badge codes=200

Auth

VQuestionResultObserved
Email enumeration via magic-link start (existing vs non-existing email)✓ Same response (no enumeration)nonex: 404 | exist: 404
QA login with path-traversal role param✓ Blocked (401)http=401
QA login with empty password✓ Rejected (400)http=400
QA-as with script-tag userId✓ Rejected (400 invalid id)http=400