nextjs - ✅(Solved) Fix Server Actions CSRF validation should be case insensitive [2 pull requests, 4 comments, 5 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#88599Fetched 2026-04-08 02:04:21
View on GitHub
Comments
4
Participants
5
Timeline
14
Reactions
0
Author
Timeline (top)
commented ×4cross-referenced ×2labeled ×2closed ×1

Next.js Server Actions implement CSRF protection by comparing the Origin header against the Host or x-forwarded-host header to ensure same-origin requests. However, this comparison is case-sensitive for the host header while the origin domain is normalized to lowercase by the URL API, violating RFC 1123 which mandates that hostnames are case-insensitive.

This inconsistency can lead to:

  1. False positive CSRF blocks that reject legitimate same-origin requests when hostname case doesn't match
  2. Inconsistent security behavior across different deployment configurations
  3. RFC 1123 compliance violation treating semantically identical hostnames as different

The vulnerability exists in packages/next/src/server/app-render/action-handler.ts where parseHostHeader() preserves the original case of the Host/x-forwarded-host headers, while originDomain from the Origin header is always lowercase (normalized by JavaScript's URL API).

Root Cause:

// Origin is lowercased by URL API
const originDomain = new URL(originHeader).host  // → "example.com"

// Host header case is preserved
const host = parseHostHeader(req.headers)  // → "Example.com"

// Case-sensitive comparison causes mismatch
if (originDomain !== host.value) {  // "example.com" !== "Example.com" → TRUE
  // False positive CSRF block or bypass
}

Error Message

Actual Result: Request fails with "Invalid Server Actions request" error

Root Cause

Root Cause:

// Origin is lowercased by URL API
const originDomain = new URL(originHeader).host  // → "example.com"

Fix Action

Fix / Workaround

Patch 1: Normalize Origin Domain (Primary Fix)

Patch 2: Normalize Host Headers (Required Fix)

Patch 3: Normalize in Wildcard Matcher (Defense in Depth)

PR fix notes

PR #88687: fix(server-actions): case-insensitive origin matching for CSRF protection

Description (problem / solution / changelog)

Fixes #88599

What?

Make Server Actions CSRF origin validation case-insensitive for both:

  1. Host / X-Forwarded-Host header comparison with Origin
  2. serverActions.allowedOrigins config matching

Why?

Per RFC 3986 Section 3.2.2 states the host component of URIs is case-insensitive:

"...host is case-insensitive, producers and normalizers should use lowercase for registered names..."

Per RFC 3986 Section 6.2.2.1 reinforces:

"...the scheme and host are case-insensitive and therefore should be normalized to lowercase."

(per RFC 2119) While the RFC recommends ("should") that producers normalize to lowercase, this is not mandatory. In practice, proxies may preserve the original case of Host/X-Forwarded-Host headers. Since upstream normalization is not guaranteed, the application(Next.js) performing the comparison must handle case-insensitive matching.

Current behavior in the existing code

In the current implementation, normalization via the JavaScript URL API is applied only to the Origin header value. Meanwhile, Host/X-Forwarded-Host headers retain their original casing, which can lead to false-positive CSRF blocks when proxies forward headers with different casing (e.g., Example.com vs example.com).

How?

  • Normalize Host and X-Forwarded-Host to lowercase in parseHostHeader()
  • Add defensive case-insensitive comparison in origin/host matching helper
  • (Additional) Normalize both origin and patterns to lowercase in isCsrfOriginAllowed()
    • Similar to the Origin vs Host casing mismatch, this also fixes case mismatches in serverActions.allowedOrigins values from next.config.js.

Tests

1. E2E test for Host/X-Forwarded-Host case mismatch (host-origin-case-insensitive)

To reproduce the real-world scenario where Origin and X-Forwarded-Host have different casing, added a proxy-based e2e test. The proxy sets:

  • Origin: https://example.com (lowercase, as normalized by URL API)
  • X-Forwarded-Host: Example.com (mixed case, simulating proxy behavior)

This test failed before the fix due to case-sensitive comparison, now passes.

2. E2E test for allowedOrigins config case mismatch (config-match-case-insensitive)

Added an e2e test where next.config.js specifies serverActions.allowedOrigins: ['Example.COM'] while the actual origin header is https://example.com. This verifies that the config matching is also case-insensitive.

3. Unit tests

  • action-handler.test.ts: Tests for parseHostHeader() lowercasing and isOriginMatchingHost() case-insensitive comparison
  • csrf-protection.test.ts: Tests for isCsrfOriginAllowed() case-insensitive matching between origin and allowedOrigins patterns

To reproduce (before the fix)

You can confirm the previous behavior (case-sensitive matching) by checking out the commits below and running the corresponding e2e tests.

1) Origin vs Host / X-Forwarded-Host case mismatch (e2b569a)

<details> <summary><strong>✅ Reproduction (video + logs)</strong></summary>

https://github.com/user-attachments/assets/24283fdd-cf4e-4190-9ddc-5b178f9ac445

 GET / 200 in 394ms (compile: 103ms, proxy.ts: 113ms, render: 178ms)
`x-forwarded-host` header with value `Example.com` does not match `origin` header with value `example.com` from a forwarded Server Actions request. Aborting the action.
⨯ Error: Invalid Server Actions request.
    at ignore-listed frames {
  digest: '1743102819@E80'
}
 POST / 500 in 171ms (compile: 2ms, proxy.ts: 3ms, render: 166ms)
[browser] Uncaught Error: Invalid Server Actions request.
</details>
git checkout e2b569a
pnpm test-dev-turbo test/e2e/app-dir/actions-allowed-origins/app-action-host-match-case-insensitive.test.ts

2) serverActions.allowedOrigins config case mismatch (a2d6510)

<details> <summary><strong>✅ Reproduction (video + logs)</strong></summary>

https://github.com/user-attachments/assets/5d6517e5-8329-4daf-a14a-1d841cc14902

 GET / 200 in 606ms (compile: 280ms, proxy.ts: 124ms, render: 202ms)
`x-forwarded-host` header with value `localhost:3000` does not match `origin` header with value `example.com` from a forwarded Server Actions request. Aborting the action.
⨯ Error: Invalid Server Actions request.
    at ignore-listed frames {
  digest: '3375356869@E80'
}
 POST / 500 in 201ms (compile: 4ms, proxy.ts: 3ms, render: 193ms)
[browser] Uncaught Error: Invalid Server Actions request.
</details>
git checkout a2d6510
pnpm test-dev-turbo test/e2e/app-dir/actions-allowed-origins/app-action-config-match-case-insensitive.test.ts

Changed files

  • packages/next/src/server/app-render/action-handler.test.ts (modified, +71/-1)
  • packages/next/src/server/app-render/action-handler.ts (modified, +18/-6)
  • packages/next/src/server/app-render/csrf-protection.test.ts (modified, +7/-0)
  • packages/next/src/server/app-render/csrf-protection.ts (modified, +10/-6)

PR #1: fix(server-actions): case-insensitive origin matching for CSRF protection

Description (problem / solution / changelog)

What?

Make Server Actions CSRF origin validation case-insensitive for both:

  1. Host / X-Forwarded-Host header comparison with Origin
  2. serverActions.allowedOrigins config matching

Why?

Per RFC 3986 Section 3.2.2 states the host component of URIs is case-insensitive:

"...host is case-insensitive, producers and normalizers should use lowercase for registered names..."

Per RFC 3986 Section 6.2.2.1 reinforces:

"...the scheme and host are case-insensitive and therefore should be normalized to lowercase."

(per RFC 2119) While the RFC recommends ("should") that producers normalize to lowercase, this is not mandatory. In practice, proxies may preserve the original case of Host/X-Forwarded-Host headers. Since upstream normalization is not guaranteed, the application(Next.js) performing the comparison must handle case-insensitive matching.

Current behavior in the existing code

In the current implementation, normalization via the JavaScript URL API is applied only to the Origin header value. Meanwhile, Host/X-Forwarded-Host headers retain their original casing, which can lead to false-positive CSRF blocks when proxies forward headers with different casing (e.g., Example.com vs example.com).

How?

  • Normalize Host and X-Forwarded-Host to lowercase in parseHostHeader()
  • Add defensive case-insensitive comparison in origin/host matching helper
  • (Additional) Normalize both origin and patterns to lowercase in isCsrfOriginAllowed()
    • Similar to the Origin vs Host casing mismatch, this also fixes case mismatches in serverActions.allowedOrigins values from next.config.js.

Tests

1. E2E test for Host/X-Forwarded-Host case mismatch (host-origin-case-insensitive)

To reproduce the real-world scenario where Origin and X-Forwarded-Host have different casing, added a proxy-based e2e test. The proxy sets:

  • Origin: https://example.com (lowercase, as normalized by URL API)
  • X-Forwarded-Host: Example.com (mixed case, simulating proxy behavior)

This test failed before the fix due to case-sensitive comparison, now passes.

2. E2E test for allowedOrigins config case mismatch (config-match-case-insensitive)

Added an e2e test where next.config.js specifies serverActions.allowedOrigins: ['Example.COM'] while the actual origin header is https://example.com. This verifies that the config matching is also case-insensitive.

3. Unit tests

  • action-handler.test.ts: Tests for parseHostHeader() lowercasing and isOriginMatchingHost() case-insensitive comparison
  • csrf-protection.test.ts: Tests for isCsrfOriginAllowed() case-insensitive matching between origin and allowedOrigins patterns

To reproduce (before the fix)

You can confirm the previous behavior (case-sensitive matching) by checking out the commits below and running the corresponding e2e tests.

1) Origin vs Host / X-Forwarded-Host case mismatch (e2b569a)

<details> <summary><strong>✅ Reproduction (video + logs)</strong></summary>

https://github.com/user-attachments/assets/24283fdd-cf4e-4190-9ddc-5b178f9ac445

 GET / 200 in 394ms (compile: 103ms, proxy.ts: 113ms, render: 178ms)
`x-forwarded-host` header with value `Example.com` does not match `origin` header with value `example.com` from a forwarded Server Actions request. Aborting the action.
⨯ Error: Invalid Server Actions request.
    at ignore-listed frames {
  digest: '1743102819@E80'
}
 POST / 500 in 171ms (compile: 2ms, proxy.ts: 3ms, render: 166ms)
[browser] Uncaught Error: Invalid Server Actions request.
</details>
git checkout e2b569a
pnpm test-dev-turbo test/e2e/app-dir/actions-allowed-origins/app-action-host-match-case-insensitive.test.ts

2) serverActions.allowedOrigins config case mismatch (a2d6510)

<details> <summary><strong>✅ Reproduction (video + logs)</strong></summary>

https://github.com/user-attachments/assets/5d6517e5-8329-4daf-a14a-1d841cc14902

 GET / 200 in 606ms (compile: 280ms, proxy.ts: 124ms, render: 202ms)
`x-forwarded-host` header with value `localhost:3000` does not match `origin` header with value `example.com` from a forwarded Server Actions request. Aborting the action.
⨯ Error: Invalid Server Actions request.
    at ignore-listed frames {
  digest: '3375356869@E80'
}
 POST / 500 in 201ms (compile: 4ms, proxy.ts: 3ms, render: 193ms)
[browser] Uncaught Error: Invalid Server Actions request.
</details>
git checkout a2d6510
pnpm test-dev-turbo test/e2e/app-dir/actions-allowed-origins/app-action-config-match-case-insensitive.test.ts

Fixes #issue

Changed files

  • packages/next/src/server/app-render/action-handler.test.ts (modified, +71/-1)
  • packages/next/src/server/app-render/action-handler.ts (modified, +18/-6)
  • packages/next/src/server/app-render/csrf-protection.test.ts (modified, +7/-0)
  • packages/next/src/server/app-render/csrf-protection.ts (modified, +10/-6)

Code Example

// Origin is lowercased by URL API
const originDomain = new URL(originHeader).host  // → "example.com"

// Host header case is preserved
const host = parseHostHeader(req.headers)  // → "Example.com"

// Case-sensitive comparison causes mismatch
if (originDomain !== host.value) {  // "example.com" !== "Example.com" → TRUE
  // False positive CSRF block or bypass
}

---

// app/actions.ts
'use server'

export async function testAction() {
  return { success: true, message: 'Action executed' }
}

---

// app/page.tsx
import { testAction } from './actions'

export default function Page() {
  return (
    <form action={testAction}>
      <button type="submit">Test Action</button>
    </form>
  )
}

---

# nginx.conf
proxy_set_header X-Forwarded-Host "Example.com";

---

curl -X POST http://localhost:3000/_next/data/action \
  -H "Origin: http://example.com" \
  -H "x-forwarded-host: Example.com" \
  -H "Content-Type: application/json" \
  -d '{"actionId":"abc123"}'

---

Fedora 43
Next.js main

---

// CORRECT IMPLEMENTATION (block-cross-site.ts:86-94)
const rawOrigin = req.headers['origin']

if (rawOrigin && rawOrigin !== 'null') {
  const parsedOrigin = parseUrl(rawOrigin)

  if (parsedOrigin) {
    const originLowerCase = parsedOrigin.hostname.toLowerCase()  // ✅ LOWERCASED

    if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) {
      return warnOrBlockRequest(res, originLowerCase, mode)
    }
  }
}

---

const originHeader = req.headers['origin']
  const originDomain =
    typeof originHeader === 'string' && originHeader !== 'null'
-     ? new URL(originHeader).host
+     ? new URL(originHeader).host.toLowerCase()
      : undefined

---

export function parseHostHeader(
  headers: IncomingHttpHeaders,
  originDomain?: string
) {
  const forwardedHostHeader = headers['x-forwarded-host']
  const forwardedHostHeaderValue =
    forwardedHostHeader && Array.isArray(forwardedHostHeader)
-     ? forwardedHostHeader[0]
+     ? forwardedHostHeader[0]?.toLowerCase()
-     : forwardedHostHeader?.split(',')?.[0]?.trim()
+     : forwardedHostHeader?.split(',')?.[0]?.trim()?.toLowerCase()
- const hostHeader = headers['host']
+ const hostHeader = headers['host']?.toLowerCase()

  if (originDomain) {
    return forwardedHostHeaderValue === originDomain
      ? {
          type: HostType.XForwardedHost,
          value: forwardedHostHeaderValue,
        }
      : hostHeader === originDomain
        ? {
            type: HostType.Host,
            value: hostHeader,
          }
        : undefined
  }

  return forwardedHostHeaderValue
    ? {
        type: HostType.XForwardedHost,
        value: forwardedHostHeaderValue,
      }
    : hostHeader
      ? {
          type: HostType.Host,
          value: hostHeader,
        }
      : undefined
}

---

function matchWildcardDomain(domain: string, pattern: string) {
- const domainParts = domain.split('.')
- const patternParts = pattern.split('.')
+ const domainParts = domain.toLowerCase().split('.')
+ const patternParts = pattern.toLowerCase().split('.')

---

diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts
index abc123..def456 100644
--- a/packages/next/src/server/app-render/action-handler.ts
+++ b/packages/next/src/server/app-render/action-handler.ts
@@ -471,11 +471,11 @@ export function parseHostHeader(
 ) {
   const forwardedHostHeader = headers['x-forwarded-host']
   const forwardedHostHeaderValue =
     forwardedHostHeader && Array.isArray(forwardedHostHeader)
-      ? forwardedHostHeader[0]
-      : forwardedHostHeader?.split(',')?.[0]?.trim()
-  const hostHeader = headers['host']
+      ? forwardedHostHeader[0]?.toLowerCase()
+      : forwardedHostHeader?.split(',')?.[0]?.trim()?.toLowerCase()
+  const hostHeader = headers['host']?.toLowerCase()

   if (originDomain) {
     return forwardedHostHeaderValue === originDomain
       ? {

diff --git a/packages/next/src/server/app-render/csrf-protection.ts b/packages/next/src/server/app-render/csrf-protection.ts
index abc123..def456 100644
--- a/packages/next/src/server/app-render/csrf-protection.ts
+++ b/packages/next/src/server/app-render/csrf-protection.ts
@@ -5,8 +5,8 @@
 // https://nextjs.org/docs/app/api-reference/components/image#remotepatterns
 // TODO - retrofit micromatch to work in edge and use that instead
 function matchWildcardDomain(domain: string, pattern: string) {
-  const domainParts = domain.split('.')
-  const patternParts = pattern.split('.')
+  const domainParts = domain.toLowerCase().split('.')
+  const patternParts = pattern.toLowerCase().split('.')

   if (patternParts.length < 1) {
     // pattern is empty and therefore invalid to match against
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/geeknik/nextjs-csrf-case-sensitivity-repro

To Reproduce

Summary

Next.js Server Actions implement CSRF protection by comparing the Origin header against the Host or x-forwarded-host header to ensure same-origin requests. However, this comparison is case-sensitive for the host header while the origin domain is normalized to lowercase by the URL API, violating RFC 1123 which mandates that hostnames are case-insensitive.

This inconsistency can lead to:

  1. False positive CSRF blocks that reject legitimate same-origin requests when hostname case doesn't match
  2. Inconsistent security behavior across different deployment configurations
  3. RFC 1123 compliance violation treating semantically identical hostnames as different

The vulnerability exists in packages/next/src/server/app-render/action-handler.ts where parseHostHeader() preserves the original case of the Host/x-forwarded-host headers, while originDomain from the Origin header is always lowercase (normalized by JavaScript's URL API).

Root Cause:

// Origin is lowercased by URL API
const originDomain = new URL(originHeader).host  // → "example.com"

// Host header case is preserved
const host = parseHostHeader(req.headers)  // → "Example.com"

// Case-sensitive comparison causes mismatch
if (originDomain !== host.value) {  // "example.com" !== "Example.com" → TRUE
  // False positive CSRF block or bypass
}

Steps to Reproduce

Environment Setup

  1. Clone Next.js repository
  2. Create a test application with Server Actions
  3. Configure deployment with a hostname containing uppercase letters (e.g., Example.com)

Reproduction Steps

Step 1: Create a Next.js app with a Server Action

// app/actions.ts
'use server'

export async function testAction() {
  return { success: true, message: 'Action executed' }
}
// app/page.tsx
import { testAction } from './actions'

export default function Page() {
  return (
    <form action={testAction}>
      <button type="submit">Test Action</button>
    </form>
  )
}

Step 2: Configure reverse proxy to set x-forwarded-host with uppercase letters

# nginx.conf
proxy_set_header X-Forwarded-Host "Example.com";

Step 3: Send a legitimate same-origin request with lowercase origin

curl -X POST http://localhost:3000/_next/data/action \
  -H "Origin: http://example.com" \
  -H "x-forwarded-host: Example.com" \
  -H "Content-Type: application/json" \
  -d '{"actionId":"abc123"}'

Current vs. Expected behavior

Expected Result: Request should succeed (same origin) Actual Result: Request fails with "Invalid Server Actions request" error

This inconsistency means:

  • Dev endpoints (/__nextjs_*) have correct CSRF protection
  • Server Actions have broken CSRF protection
  • Different code paths behave differently for the same security check

RFC 1123 Section 2.1 states:

"Domain names are case-insensitive"

The hostnames Example.com, example.com, and EXAMPLE.COM are semantically identical according to DNS and HTTP specifications.

Implications:

  • Violates web standards
  • Unpredictable behavior across different clients/proxies
  • Different proxies may normalize case differently (Apache, Nginx, Cloudflare, etc.)

Provide environment information

Fedora 43
Next.js main

Which area(s) are affected? (Select all that apply)

Server Actions

Which stage(s) are affected? (Select all that apply)

Other (Deployed)

Additional context

Additional Context

Comparison with Correct Implementation

The Next.js codebase already has a correct implementation of this check in packages/next/src/server/lib/router-utils/block-cross-site.ts:

// CORRECT IMPLEMENTATION (block-cross-site.ts:86-94)
const rawOrigin = req.headers['origin']

if (rawOrigin && rawOrigin !== 'null') {
  const parsedOrigin = parseUrl(rawOrigin)

  if (parsedOrigin) {
    const originLowerCase = parsedOrigin.hostname.toLowerCase()  // ✅ LOWERCASED

    if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) {
      return warnOrBlockRequest(res, originLowerCase, mode)
    }
  }
}

This demonstrates:

  1. The correct approach is already implemented elsewhere in the codebase
  2. The vulnerability is an inconsistency, not a design flaw
  3. The fix aligns with existing Next.js security patterns

References

  1. RFC 1123 - Requirements for Internet Hosts https://tools.ietf.org/html/rfc1123#section-2.1

  2. RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax https://tools.ietf.org/html/rfc3986#section-3.2.2

  3. WHATWG URL Standard - Host Parsing https://url.spec.whatwg.org/#host-parsing

  4. OWASP CSRF Prevention Cheat Sheet https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

  5. Next.js Server Actions Documentation https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

  6. CWE-178: Improper Handling of Case Sensitivity https://cwe.mitre.org/data/definitions/178.html

  7. CVSSv4.0 Specification https://www.first.org/cvss/v4.0/specification-document


Recommended Fix

Normalize all hostname values to lowercase before comparison, consistent with RFC 1123 and the existing implementation in block-cross-site.ts.

Patch 1: Normalize Origin Domain (Primary Fix)

File: packages/next/src/server/app-render/action-handler.ts

Lines 613-617:

  const originHeader = req.headers['origin']
  const originDomain =
    typeof originHeader === 'string' && originHeader !== 'null'
-     ? new URL(originHeader).host
+     ? new URL(originHeader).host.toLowerCase()
      : undefined

Note: This line already normalizes to lowercase via URL API, but making it explicit improves clarity.

Patch 2: Normalize Host Headers (Required Fix)

File: packages/next/src/server/app-render/action-handler.ts

Lines 474-506 (in parseHostHeader function):

export function parseHostHeader(
  headers: IncomingHttpHeaders,
  originDomain?: string
) {
  const forwardedHostHeader = headers['x-forwarded-host']
  const forwardedHostHeaderValue =
    forwardedHostHeader && Array.isArray(forwardedHostHeader)
-     ? forwardedHostHeader[0]
+     ? forwardedHostHeader[0]?.toLowerCase()
-     : forwardedHostHeader?.split(',')?.[0]?.trim()
+     : forwardedHostHeader?.split(',')?.[0]?.trim()?.toLowerCase()
- const hostHeader = headers['host']
+ const hostHeader = headers['host']?.toLowerCase()

  if (originDomain) {
    return forwardedHostHeaderValue === originDomain
      ? {
          type: HostType.XForwardedHost,
          value: forwardedHostHeaderValue,
        }
      : hostHeader === originDomain
        ? {
            type: HostType.Host,
            value: hostHeader,
          }
        : undefined
  }

  return forwardedHostHeaderValue
    ? {
        type: HostType.XForwardedHost,
        value: forwardedHostHeaderValue,
      }
    : hostHeader
      ? {
          type: HostType.Host,
          value: hostHeader,
        }
      : undefined
}

Patch 3: Normalize in Wildcard Matcher (Defense in Depth)

File: packages/next/src/server/app-render/csrf-protection.ts

Lines 6-8:

function matchWildcardDomain(domain: string, pattern: string) {
- const domainParts = domain.split('.')
- const patternParts = pattern.split('.')
+ const domainParts = domain.toLowerCase().split('.')
+ const patternParts = pattern.toLowerCase().split('.')

Complete Patch File

diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts
index abc123..def456 100644
--- a/packages/next/src/server/app-render/action-handler.ts
+++ b/packages/next/src/server/app-render/action-handler.ts
@@ -471,11 +471,11 @@ export function parseHostHeader(
 ) {
   const forwardedHostHeader = headers['x-forwarded-host']
   const forwardedHostHeaderValue =
     forwardedHostHeader && Array.isArray(forwardedHostHeader)
-      ? forwardedHostHeader[0]
-      : forwardedHostHeader?.split(',')?.[0]?.trim()
-  const hostHeader = headers['host']
+      ? forwardedHostHeader[0]?.toLowerCase()
+      : forwardedHostHeader?.split(',')?.[0]?.trim()?.toLowerCase()
+  const hostHeader = headers['host']?.toLowerCase()

   if (originDomain) {
     return forwardedHostHeaderValue === originDomain
       ? {

diff --git a/packages/next/src/server/app-render/csrf-protection.ts b/packages/next/src/server/app-render/csrf-protection.ts
index abc123..def456 100644
--- a/packages/next/src/server/app-render/csrf-protection.ts
+++ b/packages/next/src/server/app-render/csrf-protection.ts
@@ -5,8 +5,8 @@
 // https://nextjs.org/docs/app/api-reference/components/image#remotepatterns
 // TODO - retrofit micromatch to work in edge and use that instead
 function matchWildcardDomain(domain: string, pattern: string) {
-  const domainParts = domain.split('.')
-  const patternParts = pattern.split('.')
+  const domainParts = domain.toLowerCase().split('.')
+  const patternParts = pattern.toLowerCase().split('.')

   if (patternParts.length < 1) {
     // pattern is empty and therefore invalid to match against

extent analysis

TL;DR

To fix the case sensitivity issue in Next.js Server Actions' CSRF protection, normalize all hostname values to lowercase before comparison.

Guidance

  1. Apply the recommended patches: Update packages/next/src/server/app-render/action-handler.ts and packages/next/src/server/app-render/csrf-protection.ts with the provided patches to normalize hostname values.
  2. Verify the fix: Test the application with different case variations in the hostname to ensure that the CSRF protection works as expected.
  3. Review existing implementations: Check other parts of the codebase for similar case sensitivity issues and apply the same normalization approach.
  4. Test with various deployment configurations: Validate the fix across different deployment setups, including those with reverse proxies, to ensure consistent behavior.

Example

The provided patches demonstrate how to normalize hostname values to lowercase. For instance, in action-handler.ts, the parseHostHeader function is updated to use toLowerCase() when processing the x-forwarded-host and host headers.

Notes

  • The fix aligns with existing Next.js security patterns and RFC 1123, which mandates case-insensitive hostnames.
  • Normalizing hostnames to lowercase ensures consistent behavior across different clients, proxies, and deployment configurations.

Recommendation

Apply the workaround by implementing the provided patches to normalize hostname values to lowercase, ensuring consistent and secure CSRF protection in Next.js Server Actions.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - ✅(Solved) Fix Server Actions CSRF validation should be case insensitive [2 pull requests, 4 comments, 5 participants]