Skip to content

fix: use segment-based domain matching for CSRF origin wildcard patterns#821

Merged
james-elicx merged 1 commit intomainfrom
fix/csrf-origin-wildcard-matching
Apr 12, 2026
Merged

fix: use segment-based domain matching for CSRF origin wildcard patterns#821
james-elicx merged 1 commit intomainfrom
fix/csrf-origin-wildcard-matching

Conversation

@southpolesteve
Copy link
Copy Markdown
Collaborator

Summary

Replace suffix-based wildcard origin matching with proper segment-by-segment DNS domain matching for server action CSRF origin validation.

Details

isOriginAllowed() used String.endsWith() for wildcard patterns, so *.example.com would match any hostname ending with .example.com, including evil.example.com.attacker.com.

The new matchWildcardDomain() implementation splits both domain and pattern by . and compares segment-by-segment from right to left:

  • * matches exactly one DNS label
  • ** matches one or more DNS labels
  • wildcards are blocked from matching entire domains (* or ** alone)
  • matching is case-insensitive per RFC 1035

Ported from Next.js:

Tests

12 unit tests ported from Next.js covering:

  • exact match
  • single-level wildcard (*)
  • multi-level wildcard (**)
  • non-matching TLD and unrelated domains
  • empty/undefined patterns
  • wildcards blocked at domain level
  • case-insensitive matching (RFC 1035)
  • localhost patterns
  • attacker-controlled suffix domains (the regression case)

The wildcard origin matching in isOriginAllowed() used endsWith() for
suffix matching, so a pattern like *.example.com would match any origin
whose hostname ended with .example.com, including attacker-controlled
domains like evil.example.com.attacker.com.

Replace with segment-by-segment domain matching ported from Next.js:
- * matches exactly one DNS label
- ** matches one or more DNS labels
- wildcards are blocked from matching entire domains
- matching is case-insensitive per RFC 1035

Ported from Next.js: packages/next/src/server/app-render/csrf-protection.ts
https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/csrf-protection.ts

Tests ported from Next.js csrf-protection.test.ts plus an explicit
regression test for the attacker-controlled suffix domain case.
@southpolesteve
Copy link
Copy Markdown
Collaborator Author

/bigbonk review this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@821

commit: a091e1f

@github-actions
Copy link
Copy Markdown

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 11, 2026

@southpolesteve Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix for a real vulnerability. The old String.endsWith() approach would match evil.example.com.attacker.com against *.example.com, which is a CSRF bypass.

The port from Next.js's csrf-protection.ts is faithful — the matchWildcardDomain implementation matches the upstream logic correctly, and the test cases are comprehensive.

Two minor nits (neither blocking):

  1. Stale JSDoc comment at line 243-246 — the old isOriginAllowed comment (Check if an origin matches any pattern...) was left as a dangling block immediately before the new matchWildcardDomain JSDoc. It should be removed or merged into the new comment since it's now orphaned.

  2. Missing undefined case label in switch — Next.js has case undefined: default: in their switch statement, while this port only has default:. Functionally equivalent since undefined falls through to default, but mentioning for completeness. Not worth changing.

The case-insensitive comparison in isOriginAllowed using .toLowerCase() (line 294) differs from matchWildcardDomain's ASCII-only replace(/[A-Z]/g, ...), but this matches the Next.js upstream where isCsrfOriginAllowed also uses the regex approach for both paths. Either way, for DNS names (ASCII-only), .toLowerCase() and the regex approach are equivalent — no issue here.

Tests are well-structured and cover all the important cases including the regression case (attacker-controlled suffix domains).

@@ -244,13 +244,54 @@ export async function validateServerActionPayload(
* Check if an origin matches any pattern in the allowed origins list.
* Supports wildcard subdomains (e.g. `*.example.com`).
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This old JSDoc block is now orphaned — it was the comment for the previous isOriginAllowed function, but now it sits above matchWildcardDomain which has its own JSDoc immediately below. Should be removed to avoid confusion.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 11, 2026

The review has been posted. Here's a summary of my review:

PR #821 Review: Approved

This PR fixes a real CSRF vulnerability where the old String.endsWith() approach for wildcard origin matching would incorrectly allow attacker-controlled domains like evil.example.com.attacker.com to match against *.example.com.

The fix properly ports Next.js's segment-by-segment domain matching from csrf-protection.ts:

  • * matches exactly one DNS label
  • ** matches one or more DNS labels
  • Wildcards are blocked from matching entire domains
  • Case-insensitive per RFC 1035
  • Right-to-left segment comparison prevents suffix attacks

All 12 ported tests pass. One minor nit was left as an inline comment: there's a stale JSDoc block (the old isOriginAllowed comment) left orphaned above the new matchWildcardDomain function that should be cleaned up.

github run

@james-elicx james-elicx merged commit 8fbffb2 into main Apr 12, 2026
33 checks passed
@james-elicx james-elicx deleted the fix/csrf-origin-wildcard-matching branch April 12, 2026 09:52
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