From 0276479e7d0e280e0f8d26fa567d3b7aa97a516f Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:58:32 +0000 Subject: [PATCH 1/2] fix(http): prevent XSRF token leakage to protocol-relative URLs The XSRF interceptor previously failed to detect protocol-relative URLs (starting with `//`) as absolute URLs. This allowed requests to such URLs to include the XSRF token, potentially leaking it to external domains. This change updates the interceptor to correctly identify protocol-relative URLs as absolute and exclude them from receiving the XSRF token. --- packages/common/http/src/xsrf.ts | 9 ++++++--- packages/common/http/test/xsrf_spec.ts | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/common/http/src/xsrf.ts b/packages/common/http/src/xsrf.ts index 88d0781aabd2..1d38594d4e10 100644 --- a/packages/common/http/src/xsrf.ts +++ b/packages/common/http/src/xsrf.ts @@ -82,11 +82,15 @@ export class HttpXsrfCookieExtractor implements HttpXsrfTokenExtractor { } } +/** + * Regex to match absolute URLs, including protocol-relative URLs. + */ +const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i; + export function xsrfInterceptorFn( req: HttpRequest, next: HttpHandlerFn, ): Observable> { - const lcUrl = req.url.toLowerCase(); // Skip both non-mutating requests and absolute URLs. // Non-mutating requests don't require a token, and absolute URLs require special handling // anyway as the cookie set @@ -95,8 +99,7 @@ export function xsrfInterceptorFn( !inject(XSRF_ENABLED) || req.method === 'GET' || req.method === 'HEAD' || - lcUrl.startsWith('http://') || - lcUrl.startsWith('https://') + ABSOLUTE_URL_REGEX.test(req.url) ) { return next(req); } diff --git a/packages/common/http/test/xsrf_spec.ts b/packages/common/http/test/xsrf_spec.ts index 0db531a862af..fe1b002baad1 100644 --- a/packages/common/http/test/xsrf_spec.ts +++ b/packages/common/http/test/xsrf_spec.ts @@ -70,6 +70,23 @@ describe('HttpXsrfInterceptor', () => { expect(req.request.headers.has('X-XSRF-TOKEN')).toEqual(false); req.flush({}); }); + + it('does not apply XSRF protection when request is absolute', () => { + interceptor + .intercept(new HttpRequest('POST', 'https://example.com/test', {}), backend) + .subscribe(); + const req = backend.expectOne('https://example.com/test'); + expect(req.request.headers.has('X-XSRF-TOKEN')).toBeFalse(); + req.flush({}); + }); + + it('does not apply XSRF protection when request is protocol relative', () => { + interceptor.intercept(new HttpRequest('POST', '//example.com/test', {}), backend).subscribe(); + const req = backend.expectOne('//example.com/test'); + expect(req.request.headers.has('X-XSRF-TOKEN')).toBeFalse(); + req.flush({}); + }); + it('does not overwrite existing header', () => { interceptor .intercept( From 136e9232c4e7285e163b7683e8725a93ef8bd599 Mon Sep 17 00:00:00 2001 From: kirjs Date: Tue, 25 Nov 2025 15:12:06 -0500 Subject: [PATCH 2/2] release: cut the v20.3.14 release --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea1a1c05540..d3145bf7c4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.3.14 (2025-11-25) + +### http + +| Commit | Type | Description | +| ------------------------------------------------------------------------------------------------ | ---- | ---------------------------------------------------- | +| [0276479e7d](https://github.com/angular/angular/commit/0276479e7d0e280e0f8d26fa567d3b7aa97a516f) | fix | prevent XSRF token leakage to protocol-relative URLs | + + + # 20.3.13 (2025-11-19) diff --git a/package.json b/package.json index 5f4b2e356fa0..f9e0f10ccdb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "20.3.13", + "version": "20.3.14", "private": true, "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular",