12 of ~16 hand-rolled CSRF checks omit csrf_token from their rejection response, breaking client-side resync app-wide #85

Open
opened 2026-09-01 00:08:16 -04:00 by jared · 0 comments
Owner

Severity: High

api/custom_fields.php, clone_ticket.php, delete_comment.php, delete_attachment.php, bulk_operation.php, generate_api_key.php, manage_templates.php, manage_recurring.php, revoke_api_key.php, manage_workflows.php, ticket_dependencies.php, and upload_attachment.php all duplicate CsrfMiddleware::validateToken() inline instead of routing through api/bootstrap.php, and their 403 rejection body is just {"success":false,"error":"Invalid CSRF token"} — no csrf_token field.

Compare to api/bootstrap.php (~lines 40-47) and its direct users (add_comment.php, update_ticket.php), which correctly echo CsrfMiddleware::getToken() on rejection specifically so lt.api's resync (assets/js/base.js:475) can recover.

Impact: Once window.CSRF_TOKEN goes stale for any reason (token expiry after ~1h idle — see related CSRF-retry issue for the exact numbers, or a write in another tab rotating the shared session-scoped token), the next write against any of these 12 endpoints returns a 403 with no recovery token in the body. The client has no way to self-heal at that point — every subsequent write to any endpoint (even ones that would normally resync fine) keeps failing until the user manually reloads the page. This is strictly broader and more severe than the already-filed #57 (notification bell's raw fetch(), one specific endpoint) — the resync mechanism is effectively only implemented on a minority of the app's write surface.

Fix: Either route all 12 of these endpoints through bootstrap.php's centralized CSRF handling (preferred — also fixes error-shape/display_errors inconsistencies other audit rounds already flagged), or at minimum add 'csrf_token' => CsrfMiddleware::getToken() to each of these 12 rejection bodies.

**Severity:** High `api/custom_fields.php`, `clone_ticket.php`, `delete_comment.php`, `delete_attachment.php`, `bulk_operation.php`, `generate_api_key.php`, `manage_templates.php`, `manage_recurring.php`, `revoke_api_key.php`, `manage_workflows.php`, `ticket_dependencies.php`, and `upload_attachment.php` all duplicate `CsrfMiddleware::validateToken()` inline instead of routing through `api/bootstrap.php`, and their 403 rejection body is just `{"success":false,"error":"Invalid CSRF token"}` — no `csrf_token` field. Compare to `api/bootstrap.php` (~lines 40-47) and its direct users (`add_comment.php`, `update_ticket.php`), which correctly echo `CsrfMiddleware::getToken()` on rejection specifically so `lt.api`'s resync (`assets/js/base.js:475`) can recover. **Impact:** Once `window.CSRF_TOKEN` goes stale for any reason (token expiry after ~1h idle — see related CSRF-retry issue for the exact numbers, or a write in another tab rotating the shared session-scoped token), the *next* write against any of these 12 endpoints returns a 403 with no recovery token in the body. The client has no way to self-heal at that point — every subsequent write to *any* endpoint (even ones that would normally resync fine) keeps failing until the user manually reloads the page. This is strictly broader and more severe than the already-filed #57 (notification bell's raw `fetch()`, one specific endpoint) — the resync mechanism is effectively only implemented on a minority of the app's write surface. **Fix:** Either route all 12 of these endpoints through `bootstrap.php`'s centralized CSRF handling (preferred — also fixes error-shape/display_errors inconsistencies other audit rounds already flagged), or at minimum add `'csrf_token' => CsrfMiddleware::getToken()` to each of these 12 rejection bodies.
jared added the priority/highsecurity labels 2026-09-08 10:15:49 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/tinker_tickets#85