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 && `"