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", 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(