Skip to content

fix: prevent CloudFront cache poisoning for Next.js RSC responses#119

Open
konokenj wants to merge 1 commit intomainfrom
fix/cloudfront-rsc-cache-key-function
Open

fix: prevent CloudFront cache poisoning for Next.js RSC responses#119
konokenj wants to merge 1 commit intomainfrom
fix/cloudfront-rsc-cache-key-function

Conversation

@konokenj
Copy link
Contributor

Summary

Prevent CloudFront cache poisoning between HTML and RSC flight responses by adding a CloudFront Function that hashes Next.js RSC headers into a single cache key header.

Closes #100
Supersedes #118

Problem

Next.js App Router sends two types of requests to the same URL:

  1. HTML requests — full page loads
  2. RSC requests — client-side navigation with RSC: 1 header, returning text/x-component flight data

Next.js sets Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch (plus next-url for interception routes) to signal that responses differ based on these headers. However, CloudFront does not honor Vary — headers must be explicitly included in the Cache Policy to become part of the cache key.

Without this fix, when CloudFront caching is active (static pages, ISR, or explicit cache headers), an RSC response can be cached and served for a normal HTML request, or vice versa.

Approach

Adding all 5 RSC headers directly to the Cache Policy would hit CloudFront's 10-header limit (5 existing + 5 = 10, no room for future additions). Instead, we use a CloudFront Function (VIEWER_REQUEST) that:

  1. Reads the 5 Next.js RSC headers (rsc, next-router-prefetch, next-router-state-tree, next-router-segment-prefetch, next-url)
  2. Hashes them into a single x-nextjs-cache-key header using FNV-1a
  3. The Cache Policy includes only x-nextjs-cache-key (6/10 headers used)

This is the same approach used by cdk-nextjs (which hashes into x-open-next-cache-key).

Why CloudFront Function (not Lambda@Edge)?

The existing sign-payload Lambda@Edge (ORIGIN_REQUEST) handles request body hashing for SigV4, which requires body access — only possible with Lambda@Edge. The RSC header hashing is a lightweight header-only operation ideal for CloudFront Functions. Both coexist on the same behavior (CF Function at VIEWER_REQUEST, L@E at ORIGIN_REQUEST).

Files changed

  • cdk/lib/constructs/cf-lambda-furl-service/cf-function/cache-key.js — New CloudFront Function
  • cdk/lib/constructs/cf-lambda-furl-service/service.ts — Wire up CF Function + add x-nextjs-cache-key to Cache Policy

Grounding / References

  • Next.js source (v16.1.6) base-server.js:setVaryHeader() — Confirms Vary includes rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch for all App Router pages, plus next-url for interception routes
  • Next.js source app-render.js:149next-router-segment-prefetch: /_tree triggers a different response (route tree only), confirming it must be in the cache key
  • CVE-2025-49005 (Vercel, GHSA-r2fc-ccr8-96c4) — Cache poisoning via missing Vary header in Next.js 15.3.0–15.3.3. Workaround: manually set Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch
  • Running Next.js behind AWS CloudFront — Documents the same cache poisoning issue and fix
  • cdk-nextjs NextjsDistribution.ts — Uses the same hash-into-single-header approach with x-open-next-cache-key
  • CloudFront quotas — Cache Policy allows max 10 headers

@konokenj konokenj added this to the v2-fix milestone Mar 20, 2026
@konokenj konokenj force-pushed the fix/cloudfront-rsc-cache-key-function branch from 1e02670 to 3b6da4f Compare March 20, 2026 03:34
Add a CloudFront Function (VIEWER_REQUEST) that hashes Next.js RSC
headers (rsc, next-router-prefetch, next-router-state-tree,
next-router-segment-prefetch, next-url) into a single x-nextjs-cache-key
header included in the Cache Policy.

This approach avoids CloudFront's 10-header limit on Cache Policies
(currently 5 used + 1 added = 6/10) while correctly separating cache
entries for HTML vs RSC flight responses.

Closes #100
@konokenj konokenj force-pushed the fix/cloudfront-rsc-cache-key-function branch from 3b6da4f to 36e0905 Compare March 20, 2026 03:36
@konokenj
Copy link
Contributor Author

konokenj commented Mar 20, 2026

Verification

Deployed and verified with a temporary static page (/static-test, no cookies() call, cache-control: s-maxage=31536000).

curl

# HTML request → text/html, cache hit on 2nd request
GET /static-test → 200, content-type: text/html, x-cache: Hit from cloudfront

# RSC request → text/x-component, separate cache entry
GET /static-test (RSC: 1) → 200, content-type: text/x-component, x-cache: Hit from cloudfront

# Prefetch request → separate cache entry from both above
GET /static-test (RSC: 1, Next-Router-Prefetch: 1) → 200, content-type: text/x-component

No cross-contamination: HTML requests never received RSC payloads, and vice versa.

playwright-cli (browser)

  1. Direct navigation to /static-test → HTML rendered correctly
  2. Client-side navigation from /sign-in/static-test (via <Link>) → page rendered correctly via RSC, no JSON/flight data displayed
  3. After both cached, repeated the above — still correct

@konokenj konokenj requested a review from tmokmss March 20, 2026 03:39
@konokenj
Copy link
Contributor Author

@tmokmss ヘッダー数が多くなったのでcdk-nextjsを参考にCF Functionで実装しました。確認いただけますか?

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.

CloudFront CachePolicyにNext.js RSCヘッダーが含まれておらずキャッシュ汚染が発生する

1 participant