Commit 70cddda
authored
fix: prevent CloudFront cache poisoning for Next.js RSC responses (#119)
## 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](https://github.com/jetbridge/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:149` —
`next-router-segment-prefetch: /_tree` triggers a different response
(route tree only), confirming it must be in the cache key
- **CVE-2025-49005**
([Vercel](https://vercel.com/changelog/cve-2025-49005),
[GHSA-r2fc-ccr8-96c4](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](https://www.bstefanski.com/blog/running-nextjs-behind-aws-cloudfront)**
— Documents the same cache poisoning issue and fix
- **[cdk-nextjs](https://github.com/jetbridge/cdk-nextjs)**
`NextjsDistribution.ts` — Uses the same hash-into-single-header approach
with `x-open-next-cache-key`
- **[CloudFront
quotas](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html)**
— Cache Policy allows max 10 headers1 parent 87abeb9 commit 70cddda
File tree
5 files changed
+188
-0
lines changed- cdk
- lib/constructs/cf-lambda-furl-service
- cf-function
- test/__snapshots__
5 files changed
+188
-0
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
Lines changed: 39 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
11 | 15 | | |
12 | 16 | | |
13 | 17 | | |
14 | 18 | | |
15 | 19 | | |
| 20 | + | |
16 | 21 | | |
17 | 22 | | |
18 | 23 | | |
| |||
89 | 94 | | |
90 | 95 | | |
91 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
92 | 100 | | |
93 | 101 | | |
94 | 102 | | |
95 | 103 | | |
96 | 104 | | |
97 | 105 | | |
98 | 106 | | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
99 | 115 | | |
100 | 116 | | |
101 | 117 | | |
102 | 118 | | |
103 | 119 | | |
104 | 120 | | |
105 | 121 | | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
106 | 128 | | |
107 | 129 | | |
108 | 130 | | |
| |||
Lines changed: 63 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3087 | 3087 | | |
3088 | 3088 | | |
3089 | 3089 | | |
| 3090 | + | |
| 3091 | + | |
| 3092 | + | |
| 3093 | + | |
| 3094 | + | |
| 3095 | + | |
| 3096 | + | |
| 3097 | + | |
| 3098 | + | |
| 3099 | + | |
| 3100 | + | |
3090 | 3101 | | |
3091 | 3102 | | |
3092 | 3103 | | |
| |||
3307 | 3318 | | |
3308 | 3319 | | |
3309 | 3320 | | |
| 3321 | + | |
| 3322 | + | |
| 3323 | + | |
| 3324 | + | |
| 3325 | + | |
| 3326 | + | |
| 3327 | + | |
| 3328 | + | |
| 3329 | + | |
| 3330 | + | |
| 3331 | + | |
| 3332 | + | |
| 3333 | + | |
| 3334 | + | |
| 3335 | + | |
| 3336 | + | |
| 3337 | + | |
| 3338 | + | |
| 3339 | + | |
| 3340 | + | |
| 3341 | + | |
| 3342 | + | |
| 3343 | + | |
| 3344 | + | |
| 3345 | + | |
| 3346 | + | |
| 3347 | + | |
| 3348 | + | |
| 3349 | + | |
| 3350 | + | |
| 3351 | + | |
| 3352 | + | |
| 3353 | + | |
| 3354 | + | |
| 3355 | + | |
| 3356 | + | |
| 3357 | + | |
| 3358 | + | |
| 3359 | + | |
| 3360 | + | |
| 3361 | + | |
| 3362 | + | |
| 3363 | + | |
| 3364 | + | |
| 3365 | + | |
| 3366 | + | |
| 3367 | + | |
| 3368 | + | |
| 3369 | + | |
| 3370 | + | |
| 3371 | + | |
3310 | 3372 | | |
3311 | 3373 | | |
3312 | 3374 | | |
| |||
4202 | 4264 | | |
4203 | 4265 | | |
4204 | 4266 | | |
| 4267 | + | |
4205 | 4268 | | |
4206 | 4269 | | |
4207 | 4270 | | |
| |||
Lines changed: 63 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2922 | 2922 | | |
2923 | 2923 | | |
2924 | 2924 | | |
| 2925 | + | |
| 2926 | + | |
| 2927 | + | |
| 2928 | + | |
| 2929 | + | |
| 2930 | + | |
| 2931 | + | |
| 2932 | + | |
| 2933 | + | |
| 2934 | + | |
| 2935 | + | |
2925 | 2936 | | |
2926 | 2937 | | |
2927 | 2938 | | |
| |||
3152 | 3163 | | |
3153 | 3164 | | |
3154 | 3165 | | |
| 3166 | + | |
| 3167 | + | |
| 3168 | + | |
| 3169 | + | |
| 3170 | + | |
| 3171 | + | |
| 3172 | + | |
| 3173 | + | |
| 3174 | + | |
| 3175 | + | |
| 3176 | + | |
| 3177 | + | |
| 3178 | + | |
| 3179 | + | |
| 3180 | + | |
| 3181 | + | |
| 3182 | + | |
| 3183 | + | |
| 3184 | + | |
| 3185 | + | |
| 3186 | + | |
| 3187 | + | |
| 3188 | + | |
| 3189 | + | |
| 3190 | + | |
| 3191 | + | |
| 3192 | + | |
| 3193 | + | |
| 3194 | + | |
| 3195 | + | |
| 3196 | + | |
| 3197 | + | |
| 3198 | + | |
| 3199 | + | |
| 3200 | + | |
| 3201 | + | |
| 3202 | + | |
| 3203 | + | |
| 3204 | + | |
| 3205 | + | |
| 3206 | + | |
| 3207 | + | |
| 3208 | + | |
| 3209 | + | |
| 3210 | + | |
| 3211 | + | |
| 3212 | + | |
| 3213 | + | |
| 3214 | + | |
| 3215 | + | |
| 3216 | + | |
3155 | 3217 | | |
3156 | 3218 | | |
3157 | 3219 | | |
| |||
4026 | 4088 | | |
4027 | 4089 | | |
4028 | 4090 | | |
| 4091 | + | |
4029 | 4092 | | |
4030 | 4093 | | |
4031 | 4094 | | |
| |||
0 commit comments