Skip to content

fix(security): reject cross-origin requests in TrustedLocal#6

Merged
enowdev merged 1 commit into
enowdev:mainfrom
aguung:fix/api-csrf-origin-check
Jul 10, 2026
Merged

fix(security): reject cross-origin requests in TrustedLocal#6
enowdev merged 1 commit into
enowdev:mainfrom
aguung:fix/api-csrf-origin-check

Conversation

@aguung

@aguung aguung commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Any web page open in the user's browser could fetch() the local /api and be treated as trusted, because TrustedLocal only checked that the TCP peer was loopback and never checked Origin.
  • Adds an Origin-vs-Host check to TrustedLocal, the single choke point already used by Authorized/Require for every /api route.

Problem

  • server/middleware/dashboard.go's TrustedLocal grants full /api trust to any loopback-peer request, with no check that it actually originated from enowX's own UI.
  • Because no handler restricts Content-Type, a cross-site "simple" request (e.g. Content-Type: text/plain) is delivered by the browser without a CORS preflight even though the attacker page can't read the response — the side effect already happened.
  • Concrete impact: server/handlers/api_agent.go's Exec (arbitrary shell command) and FSWrite (arbitrary file write) sit behind this same gate — a single malicious tab open on the user's machine is drive-by RCE. The same hole reaches every other TrustedLocal-gated endpoint (accounts, keys, mitm, integrations, plugins, market).

Solution

  • originTrusted(r): if Origin is absent, trust as before (curl, the enx CLI, and local scripts never send an Origin header). If present, it must match the request's own Host — a browser cannot forge Origin, so a mismatch reliably signals a cross-site request even when the TCP peer is genuinely loopback.
  • Comparing against r.Host rather than a hardcoded 127.0.0.1/localhost allow-list keeps this correct for every way a user legitimately reaches the app locally (127.0.0.1:PORT, localhost:PORT, [::1]:PORT, or a custom ENOWX_HOST).
  • Mirrors the same-origin default already enforced by the WebSocket terminal (server/handlers/api_terminal.go, via nhooyr.io/websocket's default OriginPatterns behavior) — this brings the plain-HTTP /api path to parity with it instead of inventing a new policy.

Changed files

File Change
server/middleware/dashboard.go Adds originTrusted; TrustedLocal now also requires it.
server/middleware/dashboard_test.go New table-driven tests covering no-Origin, matching-Origin, mismatched host/port, null, malformed, and forwarded+matching-Origin cases.

Known limitation

  • This compares Origin and Host for consistency, not against a fixed allow-list, so a DNS-rebinding attack (attacker domain resolved to 127.0.0.1) would present a matching-but-attacker-chosen Origin/Host pair and bypass this check. Flagging for visibility; considered out of scope for this fix.

Test plan

  • go test ./server/middleware/... -run TestTrustedLocal -v passes (9/9, including 1 pre-existing subtest)
  • go build ./... and go test ./... pass with no regressions (41 packages)
  • Manually verified against a running instance: a no-Origin request and a matching-Origin request to /api/agent/exec both still execute; a mismatched-Origin request now gets 401 instead of executing

TrustedLocal only checked that the TCP peer was loopback, so any web
page open in the user's browser could fetch() the local /api and be
treated as trusted -- a drive-by CSRF into RCE via /api/agent/exec and
every other TrustedLocal-gated endpoint. Origin cannot be forged by a
browser, so reject a request whose Origin host differs from its own
Host; a request with no Origin header (curl, enx, scripts) is
unaffected.
@enowdev enowdev merged commit c171ded into enowdev:main Jul 10, 2026
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.

2 participants