Skip to content

fix(csrf): return 403 on CSRF rejection instead of silent empty 200#2692

Open
elibosley wants to merge 1 commit into
unraid:masterfrom
elibosley:fix/api-status-csrf
Open

fix(csrf): return 403 on CSRF rejection instead of silent empty 200#2692
elibosley wants to merge 1 commit into
unraid:masterfrom
elibosley:fix/api-status-csrf

Conversation

@elibosley

@elibosley elibosley commented Jul 14, 2026

Copy link
Copy Markdown
Member

Problem

When local_prepend.php rejects a POST for a missing/invalid CSRF token, it logs the error and exits with no status code — so the client receives an empty HTTP 200. A fetch/XHR caller can't distinguish that from a genuine empty result.

Concretely: Settings → Management Access → Unraid API Status issues a fetch POST to unraid-api.php. When that POST is blocked by the CSRF gate, the empty 200 is read as "the service returned nothing" and the panel renders Not Running — even though the API is up and the request was simply blocked. (Full first-paint reproduction and the client-side fix are in unraid/api#2039.)

Change

csrf_terminate() now returns an explicit 403 with a small JSON body ({"error":"<reason> csrf_token"}) instead of a silent empty 200.

Validation logic is unchanged — the gate still rejects the same requests and still logs the same wrong csrf_token line. Only the response shape of an already-rejected request changes, so a blocked read is observable and can't masquerade as a real (empty) response. A legitimate request carrying a valid token never reaches this path.

headers_sent() is guarded so this is a no-op if output already began.

Testing

Note / possible follow-up

The token is read as $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN']. ?? only falls through on null, not on an empty string, so a caller that sends a blank csrf_token= body field overrides a valid x-csrf-token header. If we want the header to win in that case, ($_POST['csrf_token'] ?? '') ?: ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? null) would do it without weakening validation. Left out of this PR to keep the change minimal — happy to add if desired.

Summary by CodeRabbit

  • Bug Fixes
    • CSRF validation failures now return a clear JSON error response with HTTP status 403.
    • Clients can reliably distinguish CSRF errors from legitimate empty responses.

When local_prepend.php rejects a POST for a bad/missing CSRF token it logged
the error and `exit`ed with no status code, so the client received an empty
HTTP 200. A fetch-based caller can't tell that apart from a genuine empty
result — e.g. the Unraid API Status panel renders "Not Running" (service
down) when the request was actually just blocked by the CSRF gate.

Return an explicit 403 with a small JSON body so the rejection is
observable and can't masquerade as a real response. Validation logic is
unchanged — the gate still rejects and still logs; only the response shape
of a rejected request changes.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4b252c90-ba6c-4320-8ee2-14e2dab0542d

📥 Commits

Reviewing files that changed from the base of the PR and between 369f0b2 and e7949d4.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix/include/local_prepend.php

Walkthrough

The CSRF termination helper now responds with HTTP 403 and a JSON body containing the failure reason, while setting the JSON content type when headers are available.

Changes

CSRF response handling

Layer / File(s) Summary
Structured CSRF termination response
emhttp/plugins/dynamix/include/local_prepend.php
csrf_terminate now emits a 403 JSON response with the provided CSRF failure reason instead of an empty termination response.

Estimated code review effort: 1 (Trivial) | ~2 minutes

Poem

A rabbit finds a blocked front door,
And stamps a neat four-oh-three score.
“Here’s why,” it says, in JSON bright,
Then bounds away into the night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: CSRF rejections now return HTTP 403 instead of a silent empty 200 response.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🔧 PR Test Plugin Available

A test plugin has been generated for this PR that includes the modified files.

Version: 2026.07.14.0356
Build: View Workflow Run

📥 Installation Instructions:

Install via Unraid Web UI:

  1. Go to Plugins → Install Plugin
  2. Copy and paste this URL:
https://preview.dl.unraid.net/pr-plugins/pr-2692/webgui-pr-2692.plg
  1. Click Install

Alternative: Direct Download

⚠️ Important Notes:

  • Testing only: This plugin is for testing PR changes
  • Backup included: Original files are automatically backed up
  • Easy removal: Files are restored when plugin is removed
  • Conflicts: Remove this plugin before installing production updates
  • Post-merge behavior: This preview stays available after merge until preview storage expires or it is manually cleaned up

📝 Modified Files:

Click to expand file list
emhttp/plugins/dynamix/include/local_prepend.php

🔄 To Remove:

Navigate to Plugins → Installed Plugins and remove webgui-pr-2692, or run:

plugin remove webgui-pr-2692

🤖 This comment is automatically generated and will be updated with each new push to this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant