aws-amplify / aws-amplify/amplify-hosting

CloudFront caches Set-Cookie headers in SSR responses despite cookies not in cache key - cross-user token leakage

Open
#4,100 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Dockerfile
Stars
481
Forks
123
PR merge metrics
No merged PRs in 30d

Description

## Summary

When using AWS Amplify Hosting with Next.js 15 (App Router, SSR), CloudFront caches `Set-Cookie` headers from middleware-generated responses and serves them to other users on cache hits. This causes **cross-user authentication token leakage**: User B receives User A's session tokens (access token, ID token, refresh token) via cached `Set-Cookie` headers.

This occurs even though the Amplify Console shows **"Cookies in cache key: Not enabled"**, which per [CloudFront documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Cookies.html) should strip `Set-Cookie` headers from cached responses.

## Environment

- **Amplify Hosting feature**: SSR
- **Frontend framework**: Next.js 15 (App Router)
- **Next.js version**: 15.5.9
- **Runtime**: Node.js (Lambda SSR)
- **Cache key settings (Amplify Console)**: Cookies in cache key - **Not enabled**

## Describe the bug

### Root cause chain

1. Next.js App Router pages that do not call `cookies()` or `headers()` in server components are treated as **static (prerendered)** and automatically receive `Cache-Control: s-maxage=31536000`.

2. Next.js middleware runs on every request (including requests where cookies are not in the CloudFront cache key). When the middleware performs a token refresh (e.g., access token expired, refresh token still valid), it writes new tokens to the response via `Set-Cookie` headers on a `NextResponse.next()` object.

3. The final response returned to CloudFront contains:
- `Cache-Control: s-maxage=31536000` (from Next.js prerender)
- `Set-Cookie: ` (from middleware)
- `Set-Cookie: ` (from middleware)
- `Set-Cookie: ` (from middleware)
- Static page HTML

4. CloudFront caches this **200** response (including `Set-Cookie` headers) for 1 year. Note: 307 redirect responses from middleware are NOT cached by CloudFront — only 200 responses with `s-maxage` are cached.

5. When a **different user** requests the same URL, CloudFront returns the cached 200 response — **including the first user's `Set-Cookie` headers**. The second user's browser stores the first user's tokens.

6. Because cookies are **not** in the cache key, requests with **any** cookie value (or no cookies at all) all hit the same cache entry. The cached `Set-Cookie` headers are returned to every requester.

### Why this contradicts CloudFront documentation

Per [CloudFront cookies documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Cookies.html):

> **Don't forward cookies to your origin** – CloudFront doesn't cache your objects based on cookie sent by the viewer. In addition, CloudFront removes cookies before forwarding requests to your origin, and **removes Set-Cookie headers from responses before returning responses to your viewers**.

The Amplify Console shows cookies are **not enabled** in the cache key. According to the documentation, `Set-Cookie` headers should be stripped. However, our testing shows they are **not stripped** — they are cached and returned to other users.

### Verification of cache key behavior

We verified that cookies are indeed **not** in the CloudFront cache key (consistent with the Amplify Console setting):

- Requests to the same URL with **different** cookie values all return `x-cache: Hit from cloudfront` (same cache entry)
- Requests to the same URL with **no** cookies also return `x-cache: Hit from cloudfront` (same cache entry)

However, the `Set-Cookie` headers from the cached response are **not stripped** — they are returned to every requester on cache hits.

## Reproduction steps

### Prerequisites

- A deployed Next.js app on Amplify Hosting with middleware that sets cookies on responses
- A valid refresh token cookie for an authenticated user

### Step 1: Generate a cached response with Set-Cookie

Send a request with a valid refresh token cookie to a prerendered page URL (use a unique query param to ensure a cache miss):

```bash
curl -sI -b "__Host-cb_rt=" \
"https://.amplifyapp.com/performance/list?repro-$(date +%s)"
```

**Expected response:**
```
HTTP/2 200
cache-control: s-maxage=31536000
set-cookie: __Host-cb_at=; ...
set-cookie: __Host-cb_it=; ...
set-cookie: __Host-cb_rt=; ...
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-cache: Miss from cloudfront
```

CloudFront caches this response (including `Set-Cookie` headers).

### Step 2: Request the same URL with different (fake) cookies

```bash
curl -sI -b "__Host-cb_at=FAKE_USER_B_TOKEN; __Host-cb_rt=FAKE_USER_B_RT" \
"https://.amplifyapp.com/performance/list?repro-"
```

**Observed response:**
```
HTTP/2 200
cache-control: s-maxage=31536000
set-cookie: __Host-cb_at=; ... ← User A's token!
set-cookie: __Host-cb_it=; ... ← User A's token!
set-cookie: __Host-cb_rt=; ... ← User A's token!
x-cache: Hit from cloudfront
```

The second request receives **User A's tokens** via cached `Set-Cookie` headers, despite sending completely different cookies.

### Step 3: Request the same URL with no cookies

```bash
curl -sI "https://.amplifyapp.com/performance/list?repro-"
```

**Observed response:**
```
HTTP/2 200
cache-control: s-maxage=31536000
set-cookie: __Host-cb_at=; ... ← User A's token!
x-cache: Hit from cloudfront
```

Even an anonymous request (no cookies) receives User A's tokens.

## Expected behavior

Per CloudFront documentation, when cookies are not in the cache key:
- `Set-Cookie` headers should be **stripped** from cached responses before returning to viewers
- OR the response should not be cached at all if it contains `Set-Cookie` headers

## Actual behavior

- `Set-Cookie` headers are **cached** alongside the response body
- Cached `Set-Cookie` headers are **returned to all users** on cache hits, regardless of their cookie values
- This results in **cross-user authentication token leakage**

## Impact

- **Severity: Critical (Security)**
- Any user's authentication tokens (access token, refresh token) can be served to other users or anonymous visitors via CloudFront cache
- In our case, this caused users to be logged in as a different user they had never authenticated as
- The cached response persists for `s-maxage=31536000` (1 year) unless the deployment is updated
- The leak window is whenever a user's access token expires and middleware performs a silent refresh — the refreshed tokens are cached and served to all subsequent visitors of the same URL

## Workaround

Add `Cache-Control: no-store` to all middleware responses to prevent CloudFront from caching responses with `Set-Cookie` headers:

```typescript
// middleware.ts
export async function middleware(request: NextRequest) {
// ... existing logic ...

const response = NextResponse.next();
response.headers.set('Cache-Control', 'no-store, must-revalidate');

// ... performRefresh writes Set-Cookie to response ...

return response;
}
```

Note: This workaround disables CDN caching for all pages that pass through middleware, which may impact performance. A better fix would be for Amplify to either:
1. Strip `Set-Cookie` headers from cached responses when cookies are not in the cache key (as per CloudFront documentation), or
2. Allow users to configure a custom CloudFront response headers policy, or
3. Automatically set `Cache-Control: no-store` on responses that contain `Set-Cookie` headers

## Additional context

- This issue is related to [#3544](https://github.com/aws-amplify/amplify-hosting/issues/3544) (request for custom CloudFront cache policy) and [#4041](https://github.com/aws-amplify/amplify-hosting/issues/4041) (request for query param filtering in cache policy)
- A similar issue was reported in the Supabase ecosystem: [supabase-js#1682](https://github.com/supabase/supabase-js/issues/1682) and [nuxt-modules/supabase#462](https://github.com/nuxt-modules/supabase/issues/462) — SSR framework + serverless CDN caching caused cross-user JWT leakage
- The core problem is that Amplify's managed CloudFront cache policy does not strip `Set-Cookie` headers from cached responses when cookies are not in the cache key, contradicting CloudFront's own documentation. This is a security vulnerability that affects any Amplify Hosting SSR app that sets cookies in middleware.

Contributor guide

Open the contributing guide

Research direction

No repository file or test is identified; start by reproducing the cache behavior with the provided curl requests against an Amplify SSR deployment and inspect the managed CloudFront cache policy. Done means cached responses no longer expose Set-Cookie values across users, either by stripping them or preventing caching, with the documented cookie and cache-key behavior verified.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, next.js, node.js
Domain
cloud, devops, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.