4.2 KiB
4.2 KiB
Server-Side Password Authentication Flow Assessment (Batch 3)
File inspected: crates/cgcx-server/src/main.rs
Flow Verification
1. get_metadata handler (/api/content/:cxid)
- Lines ~496–568
- Sequence:
- Content status check (Deleted/Blacklisted) → 404 NotFound
- Max-views check (
content.view_count >= max) → 410 Gone - Password validation via
password_from_request(&headers, query.sc.as_deref(), ...)→ 401 Unauthorized if invalid - Returns metadata JSON if all checks pass
- View increment: None. Metadata requests do not consume auto-destroy views.
- Conclusion: Password check is enforced and returns 401 on failure. ✅
2. serve_file handler (/api/content/:cxid/file/:file_idx)
- Lines ~700–890
- Sequence:
- Content status check → 404
- Max-views check → 410
- Password validation via
password_from_request→ 401 if invalid - Download permission check → 403 Forbidden if
?download=truebut not allowed - File lookup, path-traversal validation, ETag conditional check (304)
- Range header parsing
- View increment at line ~825:
repo.increment_views(&content_id).await? - If
new_views >= max, spawns background auto-delete task after 30s - Streams decrypted file body
- Conclusion: View increment happens after password validation and all other guards. Unauthorized requests cannot consume views. ✅
3. password_from_request helper
- Lines ~436–475
- Validates
scquery parameter using Argon2 (PasswordHash::new+Argon2::default().verify_password). - Falls back to
cgcx_pwcookie verified with HMAC-SHA256 andsubtle::ConstantTimeEq. - Returns
Noneon any mismatch, causing the caller to return 401. - Conclusion: Argon2 verification is present and constant-time. ✅
4. serve_raw_file handler (/api/content/:cxid/file/:file_idx/raw)
- Lines ~920–1020
- Sequence mirrors
serve_filefor status, max-views, and password checks. - View increment: None. This endpoint never increments
view_count.
Edge Cases & Concerns
| # | Edge Case | Impact | Risk |
|---|---|---|---|
| 1 | serve_raw_file skips view increment |
Requests to the /raw endpoint do not consume auto-destroy views. A user could repeatedly preview raw text without triggering deletion. |
Medium — bypasses view-count enforcement for text content previews. |
| 2 | Zero-size files in serve_file return early |
The zero-size branch (line ~751) returns a response before the increment block, so zero-byte files never consume a view. | Low — niche, but technically bypasses max-views for empty files. |
| 3 | Range/conditional/HEAD requests skip increment | serve_file only increments when !is_range && !is_conditional && !is_head. Video/audio seeking (range requests), cache revalidation (If-None-Match), and HEAD probes do not count as views. |
Low — intentional for UX, but means views are under-counted. |
| 4 | Max-views checked before password validation | In all three handlers, if view_count >= max, a request with a wrong password receives 410 Gone instead of 401 Unauthorized. This leaks that the content existed and exhausted its views without requiring the password. |
Low — information disclosure about content lifecycle. |
| 5 | TOCTOU race on view increment | content.view_count is read in repo.get() and incremented later in a separate statement. Under concurrent requests, two clients could both pass the initial view_count < max check and both increment, causing view_count to exceed max. Both requests are still served. |
Low — SQLite serializes writes via conn.lock(), but the read-write gap still allows one extra view. |
Conclusion
- Password authentication flow is correct. Wrong passwords receive 401 and do not increment view counts.
- No server changes are required for Batch 3. The frontend password fix (if any) is independent of server behavior.
- The identified edge cases are pre-existing behaviors. None are blockers for Batch 3, but items 1 (
serve_raw_filebypass) and 5 (TOCTOU race) may be worth addressing in a future hardening pass if auto-destroy accuracy is critical.