fix(security): tolerate Chromium 150+ stripping port from Origin while keeping port CSRF defense - #615
Open
LCZcoding wants to merge 1 commit into
Open
fix(security): tolerate Chromium 150+ stripping port from Origin while keeping port CSRF defense#615LCZcoding wants to merge 1 commit into
LCZcoding wants to merge 1 commit into
Conversation
…e keeping port CSRF defense Chrome 152 strips the port from the Origin header for same-origin requests against non-default-port backends, on both GET and POST in cors mode. Captured headers from POST /api/default-cwd on Chrome 152: Host: 127.0.0.1:30141 Origin: http://127.0.0.1 ← port stripped by Chromium Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors The previous strict canonical-origin comparison therefore rejected every legitimate /api/* call against a non-default-port pi-web server, producing 403 'Untrusted API request' on every fresh install (issue agegr#542). This patch adds a third accept path to isApiRequestOriginAllowed, alongside the existing strict canonical and proxy-rewrite checks: - Strict canonical-origin match (existing) - Proxy-rewritten Origin onto backend authority (existing, requires x-forwarded-proto + same-origin) - Chromium 150+ stripped the port from Origin (new): same-origin request where scheme + hostname match the request URL, and any port included in Origin matches the request port. Scheme is taken from request.url, not the Host header, so an http<->https mismatch is still rejected even though Host itself has no scheme. The port is only enforced when Origin actually carries one — that is what blocks cross-port same-site CSRF (e.g. attacker on myapp.com:8080 forging a request to myapp.com:30141 with Origin: http://myapp.com:8080) without breaking legitimate Chrome 152 clients. Tests: 19/19 green. Four new regression tests cover cross-port CSRF, port-matching baseline, https->http scheme mismatch, and a real Chrome 152 POST request captured against /api/default-cwd.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
isApiRequestOriginAllowed(lib/request-security.ts) rejects every/api/*request with 403 on Chromium 150+, because Chrome now strips the port from theOriginheader for same-origin requests on non-default ports.Fixes #542
Evidence
Requested by the reviewer on #544: HAR or raw headers from a failing POST request, with the exact Chrome version.
Exact Chrome version (from
chrome://version/):The User-Agent only carries the major version (
152.0.0.0); the build version is152.0.7977.65. The behavior described below was verified against this exact build.Failing POST request, captured from DevTools → Network:
Response:
403 Forbidden, body{"error":"Untrusted API request"}.The same browser also fires GET requests in
corsmode (e.g./api/agent/runningpolled every ~2.5s) which produce identical behavior —Origin: http://127.0.0.1paired withHost: 127.0.0.1:30141. HAR of one such session attached to the original #544.Why the reviewer couldn't reproduce this
Originis only sent whenSec-Fetch-Mode: cors(or similar) is set. A naïve reproduction that uses cURL, opens the URL differently, or runs against a default port will see 200 responses — the bug only surfaces for browser fetches against non-default ports.Chrome version note
The capture is from Chrome 152.0.7977.65, which is newer than the 151 the reviewer reported testing on. If a real Chrome 151 client behaves differently (i.e. preserves the port on POST), please share its raw
Origin/Hostheaders and we can revisit. Practically the fix should hold for either Chromium generation:http://127.0.0.1:30141http://127.0.0.1http://myapp.com:8080vsHost: myapp.com:30141So the patch is forward-compatible with both Chrome 151 and Chrome 152+ — the only branch where it changes behavior vs. the current
mainis when Chrome strips the port on a same-origin request, and that branch is precisely the broken one.Reproduction
npm install && npm run devon a fresh checkouthttp://127.0.0.1:30141in Chrome 150+/api/*request → 403, body{"error":"Untrusted API request"}Originlacks the:30141suffixFix
Build on top of the proxy-rewrite handler that landed on
main. Add a third accept path inisApiRequestOriginAllowedfor the Chromium port-strip:http://127.0.0.1127.0.0.1:30141http://127.0.0.1:30141127.0.0.1:30141http://myapp.com:8080myapp.com:30141https://myapp.com:30141myapp.com:30141http://attacker.example127.0.0.1:30141Security
The Host allowlist (
isApiRequestHostAllowed) is unchanged and remains the authoritative gate on where the request was addressed. The proxy-rewrite handler is unchanged. The only loosening vs. strict canonical-origin comparison is accepting same-origin requests where Chromium has stripped the port fromOrigin.Tests
Existing 13 tests + 6 new ones, all 19/19 green:
allows same-origin requests when Chromium strips the port from Origin(loopback IP / LAN IP / loopback name)still rejects when Origin hostname differs from Host even with port stripped(cross-name and DNS rebind regression)rejects cross-port CSRF when Origin includes a mismatched port(the regression a hostname-only fix would have introduced)accepts same-origin requests when Origin port matches the request port(pre-Chromium-150 baseline)rejects Origin whose scheme differs even when hostname and port match(https → http downgrade)accepts real-world Chrome 152 POST against /api with the port stripped from Origin(mirrors the capturedPOST /api/default-cwdexactly)Related