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(