diff --git a/CHANGELOG.md b/CHANGELOG.md index 51297724f6a0..7f7cbffd12ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ + + +# 20.3.26 (2026-07-08) + +### compiler-cli + +| Commit | Type | Description | +| ------------------------------------------------------------------------------------------------ | ---- | -------------------------------------- | +| [406aaa31e6](https://github.com/angular/angular/commit/406aaa31e6ac4d3c155f5ab76e315ccd8d0387fe) | fix | update babel dependencies to latest v7 | + +### core + +| Commit | Type | Description | +| ------------------------------------------------------------------------------------------------ | ---- | ----------------------------------- | +| [26831d0cbd](https://github.com/angular/angular/commit/26831d0cbd7e692210ca0799a203a7a5a0e741cd) | fix | avoid caching missing locale data | +| [8eb7aea08b](https://github.com/angular/angular/commit/8eb7aea08b27c0f1bcdeec3171880a6fcf28aa9a) | fix | reject dynamic script host elements | + +### http + +| Commit | Type | Description | +| ------------------------------------------------------------------------------------------------ | ---- | ---------------------------------------------------- | +| [b963f61028](https://github.com/angular/angular/commit/b963f61028c747843db48c6fb82965123365fd4a) | fix | prevent caching of responses with Set-Cookie headers | + +### service-worker + +| Commit | Type | Description | +| ------------------------------------------------------------------------------------------------ | ---- | ------------------------------------------ | +| [1fdf234168](https://github.com/angular/angular/commit/1fdf2341684a0f528d1d31005bd48d882c0a47d1) | fix | preserve referrer in asset requests | +| [baa093ba68](https://github.com/angular/angular/commit/baa093ba68c1d5ca7c35c562568c6021eb409b4c) | fix | preserve referrer policy in asset requests | + + + # 20.3.25 (2026-06-10) diff --git a/adev/src/content/guide/ssr.md b/adev/src/content/guide/ssr.md index 414ba83f7e3d..23d4f21c0375 100644 --- a/adev/src/content/guide/ssr.md +++ b/adev/src/content/guide/ssr.md @@ -362,7 +362,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'; @@ -482,7 +482,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. diff --git a/integration/ng_elements/package.json b/integration/ng_elements/package.json index ff6aa2fc577c..20c5665c32f3 100644 --- a/integration/ng_elements/package.json +++ b/integration/ng_elements/package.json @@ -15,7 +15,7 @@ "zone.js": "0.15.1" }, "devDependencies": { - "@babel/core": "7.28.4", + "@babel/core": "7.29.7", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-node-resolve": "^16.0.0", "@types/jasmine": "^5.0.0", diff --git a/package.json b/package.json index a9b34364d5bb..4994216c1021 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "20.3.25", + "version": "20.3.26", "private": true, "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular", @@ -73,9 +73,9 @@ "@angular/service-worker": "workspace:*", "@angular/ssr": "20.3.6", "@angular/upgrade": "workspace: *", - "@babel/cli": "7.28.3", - "@babel/core": "7.28.3", - "@babel/generator": "7.28.3", + "@babel/cli": "7.29.7", + "@babel/core": "7.29.7", + "@babel/generator": "7.29.7", "@jridgewell/sourcemap-codec": "^1.4.14", "@microsoft/api-extractor": "^7.24.2", "@rollup/plugin-babel": "^6.0.0", @@ -170,7 +170,7 @@ "@angular-devkit/architect-cli": "0.2003.6", "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#39cbcff2877b488792dd3aaf02ea818c8f023371", "@babel/plugin-proposal-async-generator-functions": "7.20.7", - "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", "@bazel/bazelisk": "^1.7.5", "@bazel/buildifier": "^8.0.0", "@bazel/ibazel": "0.26.10", diff --git a/packages/common/http/src/transfer_cache.ts b/packages/common/http/src/transfer_cache.ts index d220bc85d489..98efec082b42 100644 --- a/packages/common/http/src/transfer_cache.ts +++ b/packages/common/http/src/transfer_cache.ts @@ -232,8 +232,14 @@ export function transferCacheInterceptorFn( return event$.pipe( tap((event: HttpEvent) => { // Only cache successful HTTP responses that do not have Cache-Control - // directives that forbid shared caching (no-store or private). - if (event instanceof HttpResponse && !hasUncacheableCacheControl(event.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 ( + event instanceof HttpResponse && + !hasUncacheableCacheControl(event.headers) && + !hasSetCookieHeader(event.headers) + ) { transferState.set(storeKey, { [BODY]: event.body, [HEADERS]: getFilteredHeaders(event.headers, headersToInclude), @@ -275,6 +281,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'; } diff --git a/packages/common/http/test/transfer_cache_spec.ts b/packages/common/http/test/transfer_cache_spec.ts index d0aa26ae66ca..3e9f2fe21267 100644 --- a/packages/common/http/test/transfer_cache_spec.ts +++ b/packages/common/http/test/transfer_cache_spec.ts @@ -214,6 +214,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(); @@ -559,6 +585,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'}, diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index f993899ce075..408d1dcf9494 100644 --- a/packages/compiler-cli/package.json +++ b/packages/compiler-cli/package.json @@ -38,7 +38,7 @@ } }, "dependencies": { - "@babel/core": "7.28.3", + "@babel/core": "7.29.7", "@jridgewell/sourcemap-codec": "^1.4.14", "reflect-metadata": "^0.2.0", "chokidar": "^4.0.0", diff --git a/packages/core/src/i18n/locale_data_api.ts b/packages/core/src/i18n/locale_data_api.ts index 21bd136501a6..910a8e169ae5 100644 --- a/packages/core/src/i18n/locale_data_api.ts +++ b/packages/core/src/i18n/locale_data_api.ts @@ -102,11 +102,20 @@ export function getLocalePluralCase(locale: string): (value: number) => number { */ export function getLocaleData(normalizedLocale: string): any { if (!(normalizedLocale in LOCALE_DATA)) { - LOCALE_DATA[normalizedLocale] = + const globalLocaleData = global.ng && global.ng.common && global.ng.common.locales && global.ng.common.locales[normalizedLocale]; + // Only cache global locale data when an entry is actually found, to avoid + // caching missing lookups. In SSR this cache is process-wide across requests, + // so caching `undefined` would retain attacker-controlled locale identifiers + // indefinitely. It would also make the `in` check above short-circuit on + // subsequent lookups and skip the global fallback. + if (globalLocaleData !== undefined) { + LOCALE_DATA[normalizedLocale] = globalLocaleData; + } + return globalLocaleData; } return LOCALE_DATA[normalizedLocale]; } diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 5cca900962ad..4baea34e9f6a 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -193,6 +193,15 @@ function createHostElement(componentDef: ComponentDef, renderer: Render return createElementNode(renderer, tagName, namespace); } +function assertNotScriptHostElement(tagName: string | null | undefined): void { + if (tagName?.toLowerCase() === 'script') { + throw new RuntimeError( + RuntimeErrorCode.UNSAFE_VALUE_IN_SCRIPT, + ngDevMode && `"