Skip to content

fix(GAT-9016): allowlist the deployed origins for UI actions - #170

Merged
calmacx merged 1 commit into
devfrom
fix/GAT-9016-allowed-action-origins
Sep 30, 2026
Merged

calmacx merged 1 commit into
devfrom
fix/GAT-9016-allowed-action-origins

Conversation

@calmacx

@calmacx calmacx commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Written with Claude Code.

What

Adds allowedActionOrigins to react-router.config.ts: the prod custom domain, plus a *.europe-west1.run.app wildcard 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 .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, 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-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 Error: Bad Request at singleFetchAction (chunk-H4DAEOV7.mjs:867) in the dev logs.

allowedActionOrigins is 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 typecheck and npm 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.data against that build, varying only the Origin header. 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 local http submission still returns 200; https://evil.example.com returns 400.

Notes

  • The wildcard is deliberately broader than our own services. Matching is whole-segment — * covers exactly one segment and ** only leading segments — so no pattern can anchor on the hdr-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.
  • This 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 via TRASER_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.
  • The value is baked in at build time and cannot come from the runtime environment: the vite plugin serialises it into 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.
  • Supersedes fix(GAT-9016): serve behind a TLS-terminating proxy #169, which replaced react-router-serve with a custom express server setting trust proxy. That fixes request.url globally rather than just the guard, but it was more surface area than the problem warrants.

@gh-actions-pipelines-app

Copy link
Copy Markdown

🎉 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
calmacx force-pushed the fix/GAT-9016-allowed-action-origins branch from 41a0c91 to 4bb24d4 Compare September 30, 2026 12:19
@calmacx
calmacx merged commit 13c7352 into dev Sep 30, 2026
3 checks passed
@calmacx
calmacx deleted the fix/GAT-9016-allowed-action-origins branch September 30, 2026 12:44

This branch was successfully deployed

1 active deployment
dev — 4bb24d46 Deployed Sep 30, 2026 by calmacx via testing #826
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