Skip to content

fix(github): enforce the redirect allowlist on the client's redirect_uri - #545

Merged
viktormarinho merged 1 commit into
mainfrom
fix/oauth-redirect-allowlist-runtime-3
Aug 20, 2026
Merged

fix(github): enforce the redirect allowlist on the client's redirect_uri#545
viktormarinho merged 1 commit into
mainfrom
fix/oauth-redirect-allowlist-runtime-3

Conversation

@viktormarinho

@viktormarinho viktormarinho commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Follow-up to decocms/studio#6330, which shipped @decocms/runtime 3.0.0.

What is this contribution about?

ALLOWED_REDIRECT_HOST_SUFFIXES was checked inside authorizationUrl(), against the callback URL the runtime builds as ${url.origin}/oauth/callback. That value is always this server's own origin, so assertAllowedRedirectUri() passed unconditionally and rejected nothing. The value an attacker actually controls is the client-supplied redirect_uri on /authorize, and nothing checked that at all.

Runtime 3.0.0 validates it on both /authorize and /oauth/callback, so this passes the same suffix list in through the new allowedRedirectHosts option. The check now sits on the attacker-controlled value instead of ours.

lib/redirect-allowlist.ts and its test are deleted. The runtime's redirect-uri.ts carries the same label-boundary matching and the same cases, including the evildecocms.com and decocms.com.attacker.io near-misses.

Why zod moves too

Runtime 1.6.5 depended on "zod": "^4.0.0" and deduped with this workspace's own ^4.0.0. Runtime 3.0.0 pins 4.3.6 exactly. Left on a range we resolve 4.4.3, end up with two zod copies, and every schema crossing the runtime boundary becomes nominally incompatible: 50 type errors in server/lib/trigger-store.ts and server/main.ts that have nothing to do with this change. Pinning 4.3.6 collapses it back to one instance.

Worth knowing before the other MCPs are upgraded, since they will all hit this. The cleaner long-term fix is upstream: the runtime ships raw TypeScript and takes zod types across its API boundary, so it should express zod as a range rather than an exact pin.

How did you verify your code works?

  • bun run check github: 2 passed, 0 failed (covers github and github-repo-reports). Before the zod pin this reported 50 own-source errors; after, zero.
  • bun test github/: 112 pass, 0 fail across 7 files.
  • Remaining tsc output is 19 errors entirely inside node_modules (@decocms/runtime/src/triggers.ts, tools.ts, one ajv duplicate). scripts/check.ts filters those by design. main already had 4 of the same kind, so this is a pre-existing condition of the runtime shipping raw source, not a regression from this PR.

Runtime-side coverage lives in decocms/studio#6330: 40 tests over the exact-match rules, the allowlist near-misses, and the callback path.

How to Test

  1. bun install && bun run check github && bun test github/
  2. Deploy to staging and connect the GitHub MCP from a real client. Confirm the OAuth flow completes and a token comes back.
  3. Call /authorize with a redirect_uri on a host outside decocms.com. Expected: 400 with no location header, where it previously 302 d to GitHub.

Migration Notes

allowedRedirectHosts is what makes /authorize work at all on runtime 3.0.0 for a server with no oauth.persistence. Without it the endpoint rejects every request by design, so the config and the version bump have to ship together, which they do here.

In-flight authorizations do not survive the deploy: a state issued by the running version carries no client_id, so the new callback gate rejects it. Users retry and it works. Worth deploying off-peak.

stateSecret, which seals the state and the authorization code, is deliberately not set here. It should be a separate, later change, because mid-rollout an instance holding the secret cannot unseal a state written by one without it.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

Summary by cubic

Enforces the OAuth redirect allowlist on the client-supplied redirect_uri via @decocms/runtime 3.0.0. Previously we only validated our own callback URL (always this server), so disallowed redirects were never rejected; now the runtime validates the client value on /authorize and /oauth/callback.

  • Disallowed redirect_uri now returns 400 with no Location header (was 302 to GitHub).
  • Passes allowedRedirectHosts from ALLOWED_REDIRECT_HOST_SUFFIXES to the runtime; removes local redirect allowlist code and tests in favor of the runtime’s label-boundary rules.
  • Pins zod to "4.3.6" to match the runtime and avoid duplicate instances causing type mismatches.

Rollout/Migration

  • In-flight OAuth states from the old version will be rejected after deploy; users must retry. Prefer off-peak deploy.
  • No config action required here; the allowlist is wired. Other MCP packages upgrading to @decocms/runtime 3 should also pin zod to "4.3.6".

Written for commit ef50358. Summary will update on new commits.

Review in cubic

ALLOWED_REDIRECT_HOST_SUFFIXES was checked inside authorizationUrl(), on
the callback URL the runtime builds as `${url.origin}/oauth/callback`. That
value is always this server's own origin, so assertAllowedRedirectUri()
passed unconditionally and rejected nothing. The value an attacker actually
controls is the client's redirect_uri on /authorize, which nothing checked.

@decocms/runtime 3.0.0 validates that value on /authorize and
/oauth/callback, so hand it the same suffix list through the new
allowedRedirectHosts option. The check now sits where it does something.

Drop lib/redirect-allowlist.ts and its test; the runtime's redirect-uri.ts
carries the same label-boundary matching and the same cases.

Pin zod to 4.3.6 alongside the runtime bump. Runtime 1.6.5 depended on
"zod": "^4.0.0" and deduped with this workspace's own ^4.0.0, but 3.0.0
pins 4.3.6 exactly. Left on a range we resolve 4.4.3, get two zod copies,
and every schema crossing the runtime boundary becomes nominally
incompatible: 50 type errors in server/lib/trigger-store.ts and
server/main.ts that have nothing to do with this change. Matching the pin
collapses it back to one instance.
@viktormarinho
viktormarinho merged commit 750cc63 into main Aug 20, 2026
2 checks passed
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