mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(http): prevent caching of responses with Set-Cookie headers
Skip HttpTransferCache serialization for HTTP responses that contain a
Set-Cookie header.
Cookie-setting responses commonly represent session-specific,
user-specific, or security-sensitive state. Serializing their bodies into
SSR TransferState can embed sensitive data into the generated HTML, where
it may be reused during hydration or replayed by a shared cache/CDN.
(cherry picked from commit 80795defc6)
This commit is contained in:
@@ -432,7 +432,7 @@ To configure this, update your `angular.json` file as follows:
|
||||
You can customize how Angular caches HTTP responses during server‑side rendering (SSR) and reuses them during hydration by configuring `HttpTransferCacheOptions`.
|
||||
This configuration is provided globally using `withHttpTransferCacheOptions` inside `provideClientHydration()`.
|
||||
|
||||
By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization`, `Proxy-Authorization`, or `Cookie` headers and are not sent with `withCredentials` or Fetch API `credentials` modes that can send credentials. Angular also skips transfer cache when a request or response includes `Cache-Control` directives that forbid caching (`no-store`, `no-cache`, or `private`), or when the Fetch API `cache` option is set to `no-store` or `no-cache`. You can override the request filtering settings by using `withHttpTransferCacheOptions` in the hydration configuration.
|
||||
By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization`, `Proxy-Authorization`, or `Cookie` headers and are not sent with `withCredentials` or Fetch API `credentials` modes that can send credentials. Angular also skips transfer cache when a request or response includes `Cache-Control` directives that forbid caching (`no-store`, `no-cache`, or `private`), or when the Fetch API `cache` option is set to `no-store` or `no-cache`. Responses that carry a `Set-Cookie` header are also skipped. You can override the request filtering settings by using `withHttpTransferCacheOptions` in the hydration configuration.
|
||||
|
||||
```ts
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
@@ -560,7 +560,7 @@ To disable caching for an individual request, you can specify the [`transferCach
|
||||
httpClient.get('/api/sensitive-data', {transferCache: false});
|
||||
```
|
||||
|
||||
`HttpTransferCache` does not cache requests or responses that explicitly opt out of caching. Angular skips transfer cache entries when a request includes a `Cache-Control` header with `no-store`, `no-cache`, or `private`, or when the request uses the Fetch API `cache` option set to `no-store` or `no-cache`. Responses with `Cache-Control: no-store`, `Cache-Control: no-cache`, or `Cache-Control: private` are also not stored in the transfer cache.
|
||||
`HttpTransferCache` does not cache requests or responses that explicitly opt out of caching. Angular skips transfer cache entries when a request includes a `Cache-Control` header with `no-store`, `no-cache`, or `private`, or when the request uses the Fetch API `cache` option set to `no-store` or `no-cache`. Responses with `Cache-Control: no-store`, `Cache-Control: no-cache`, or `Cache-Control: private` are also not stored in the transfer cache. Responses that include a `Set-Cookie` header are likewise not stored, as they typically carry user-specific state.
|
||||
|
||||
NOTE: If your application uses different HTTP origins to make API calls on the server and on the client, the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token allows you to establish a mapping between those origins, so that `HttpTransferCache` feature can recognize those requests as the same ones and reuse the data cached on the server during hydration on the client.
|
||||
|
||||
|
||||
@@ -288,8 +288,10 @@ export function transferCacheInterceptorFn(
|
||||
const {headers, body, status, statusText} = event;
|
||||
|
||||
// Only cache successful HTTP responses that do not have Cache-Control
|
||||
// directives that forbid shared caching (no-store or private).
|
||||
if (hasUncacheableCacheControl(headers)) {
|
||||
// directives that forbid shared caching (no-store or private) and do not
|
||||
// carry a Set-Cookie header. A Set-Cookie header marks the response as
|
||||
// user-specific.
|
||||
if (hasUncacheableCacheControl(headers) || hasSetCookieHeader(headers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -344,6 +346,10 @@ function hasUncacheableCacheControl(headers: HttpHeaders): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function hasSetCookieHeader(headers: HttpHeaders): boolean {
|
||||
return headers.has('set-cookie');
|
||||
}
|
||||
|
||||
function isNonCacheableRequest(cache: RequestCache): boolean {
|
||||
return cache === 'no-cache' || cache === 'no-store';
|
||||
}
|
||||
|
||||
@@ -215,6 +215,32 @@ describe('TransferCache', () => {
|
||||
expect(secondNext).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not cache responses with a Set-Cookie header', () => {
|
||||
configureInterceptor();
|
||||
|
||||
const request = new HttpRequest('GET', '/test-set-cookie');
|
||||
|
||||
const firstNext = jasmine.createSpy('firstNext').and.returnValue(
|
||||
of(
|
||||
new HttpResponse({
|
||||
body: 'user-a-session',
|
||||
headers: new HttpHeaders({'Set-Cookie': 'session=user-a; HttpOnly'}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const secondNext = jasmine
|
||||
.createSpy('secondNext')
|
||||
.and.returnValue(of(new HttpResponse({body: 'user-b-session'})));
|
||||
|
||||
runOnServer(() => {
|
||||
expect(runInterceptor(request, firstNext).body).toBe('user-a-session');
|
||||
expect(runInterceptor(request, secondNext).body).toBe('user-b-session');
|
||||
});
|
||||
|
||||
expect(firstNext).toHaveBeenCalledTimes(1);
|
||||
expect(secondNext).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not cache requests with Cache-Control: no-store', () => {
|
||||
configureInterceptor();
|
||||
|
||||
@@ -591,6 +617,14 @@ describe('TransferCache', () => {
|
||||
makeRequestAndExpectOne('/test-private', 'fresh-data');
|
||||
});
|
||||
|
||||
it('should not cache responses with a Set-Cookie header', () => {
|
||||
makeRequestAndExpectOne('/test-set-cookie', 'user-a-session', {
|
||||
responseHeaders: {'Set-Cookie': 'session=user-a; HttpOnly'},
|
||||
});
|
||||
|
||||
makeRequestAndExpectOne('/test-set-cookie', 'user-b-session');
|
||||
});
|
||||
|
||||
it('should not cache responses with Cache-Control containing no-store among other directives', () => {
|
||||
makeRequestAndExpectOne('/test-multi', 'data', {
|
||||
responseHeaders: {'Cache-Control': 'max-age=0, no-store, must-revalidate'},
|
||||
|
||||
Reference in New Issue
Block a user