Repository navigation
fix(GAT-9016): allowlist the deployed origins for UI actions - #170
Merged
Merged
Conversation
|
🎉 Great job! Your PR title follows the correct format. 🚀 |
Every POST to a UI route has 400'd on Cloud Run since the react-router 7.16.0 -> 7.18.4 bump in #167. Deep Refresh, Sync New, Cancel, per-row refresh and the whole benchmark page are affected; the JSON API is not, because the guard only runs on single-fetch `.data` submissions. The guard tightened in 7.18.3. Up to 7.18.2 it compared the `Origin` header's host against the host of the request — scheme-insensitive, so a TLS-terminating proxy was invisible to it. From 7.18.3 it compares the full origin: let requestUrl = new URL(request.url); let originMatchesRequest = originUrl ? originUrl.origin === requestUrl.origin : originDomain === requestUrl.host; Cloud Run terminates TLS at the front end, so the container sees plain HTTP. `react-router-serve` never sets express's `trust proxy`, so `req.protocol` is "http" and `@react-router/express` builds `request.url` as `http://<host>/results.data` while the browser sends `Origin: https://<host>`. The schemes differ, the guard throws, and singleFetchAction returns a bare 400 the client renders as "Unexpected Server Error". `allowedActionOrigins` is matched on host alone, so listing the deployed origins clears the scheme mismatch without touching how the service is served. The Cloud Run entry is a wildcard rather than the three per-environment hostnames. Matching is whole-segment — `*` covers exactly one segment and `**` only leading segments — so no pattern can anchor on the `hdr-gateway-traser-` service-name prefix; any pattern that matches our hosts also matches every other tenant's service in the region. That exposure is accepted in exchange for a list that does not need editing when an environment or project number changes. This also drops the older `-ew.a.run.app` Cloud Run URLs, which the wildcard does not span. They still resolve and gateway-api still calls TRASER through one in `TRASER_SERVICE_URL`, but those are resource-route requests, which the guard does not apply to. Only a human browsing the admin UI at a legacy URL would see a 400. Upstream has declined to fix the proxy case (remix-run/react-router issues 15454 and 15474), so this is the supported route rather than a stopgap. The alternative, replacing react-router-serve with a custom express server that sets `trust proxy`, was prototyped in #169 and rejected as too much surface area for the problem.
calmacx
force-pushed
the
fix/GAT-9016-allowed-action-origins
branch
from
September 30, 2026 12:19
41a0c91 to
4bb24d4
Compare
This branch was successfully deployed
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.
Written with Claude Code.
What
Adds
allowedActionOriginstoreact-router.config.ts: the prod custom domain, plus a*.europe-west1.run.appwildcard covering the Cloud Run URLs for dev, preprod and prod.Why
Every POST to a UI route has 400'd on Cloud Run since the react-router 7.16.0 → 7.18.4 bump in #167 — Deep Refresh, Sync New, Cancel, per-row refresh and the whole benchmark page fail with "Unexpected Server Error". The JSON API is unaffected, because the guard only runs on single-fetch
.datasubmissions.The guard tightened in 7.18.3. Up to 7.18.2 it compared the
Originheader's host against the host of the request, which is scheme-insensitive and so blind to a TLS-terminating proxy. From 7.18.3 it compares the full origin:originUrl.origin === requestUrl.origin. Cloud Run terminates TLS at the front end, so the container sees plain HTTP;react-router-servenever sets express'strust proxy, soreq.protocolishttpand@react-router/expressbuildsrequest.urlashttp://<host>/results.datawhile the browser sendsOrigin: https://<host>. The schemes differ, the guard throws, andsingleFetchActionreturns a bare 400 — theError: Bad Request at singleFetchAction (chunk-H4DAEOV7.mjs:867)in the dev logs.allowedActionOriginsis matched on host alone, so listing the deployed origins clears the scheme mismatch without changing how the service is served. Upstream has declined to fix the proxy case (#15454, #15474), so this is the supported route rather than a stopgap.Testing
npm run lint,npm run typecheckandnpm run test:unit(28 passed) are clean, and the full integration suite passes against a production build — 13 files, 90 tests.The guard was then exercised directly with
POST /results.dataagainst that build, varying only theOriginheader. The prod custom domain and the dev, preprod and prod Cloud Run hosts all return 200, including the exact dev host from the failing logs; a same-origin localhttpsubmission still returns 200;https://evil.example.comreturns 400.Notes
*covers exactly one segment and**only leading segments — so no pattern can anchor on thehdr-gateway-traser-prefix, and any pattern matching our hosts also matches every other tenant's Cloud Run service in the region. Accepted in exchange for a list that needs no edit when an environment or project number changes.-ew.a.run.appCloud Run URLs, which the wildcard does not span. They still resolve, and gateway-api still calls TRASER through one viaTRASER_SERVICE_URL, but those are resource-route requests that the guard does not apply to. Only a human browsing the admin UI at a legacy URL would see a 400.build/server/index.js, and the guard reads it off the build module's namespace object. Runtime configuration would need a custom server wrapping the build — the approach prototyped in fix(GAT-9016): serve behind a TLS-terminating proxy #169 and rejected.react-router-servewith a custom express server settingtrust proxy. That fixesrequest.urlglobally rather than just the guard, but it was more surface area than the problem warrants.