mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(http): enable XSRF protection for same-origin absolute URLs
Previously, the XSRF interceptor only added the XSRF token to requests with relative URLs. This commit updates the interceptor to also add the token to requests with absolute URLs, provided they match the current origin.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT, ɵparseCookieValue as parseCookieValue} from '../../index';
|
||||
import {DOCUMENT, ɵparseCookieValue as parseCookieValue, PlatformLocation} from '../../index';
|
||||
import {
|
||||
EnvironmentInjector,
|
||||
inject,
|
||||
@@ -91,25 +91,28 @@ export abstract class HttpXsrfTokenExtractor {
|
||||
abstract getToken(): string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex to match absolute URLs, including protocol-relative URLs.
|
||||
*/
|
||||
const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
|
||||
|
||||
export function xsrfInterceptorFn(
|
||||
req: HttpRequest<unknown>,
|
||||
next: HttpHandlerFn,
|
||||
): Observable<HttpEvent<unknown>> {
|
||||
// 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
|
||||
// on our origin is not the same as the token expected by another origin.
|
||||
if (
|
||||
!inject(XSRF_ENABLED) ||
|
||||
req.method === 'GET' ||
|
||||
req.method === 'HEAD' ||
|
||||
ABSOLUTE_URL_REGEX.test(req.url)
|
||||
) {
|
||||
// Skip both non-mutating requests
|
||||
// Non-mutating requests generally don't require a token.
|
||||
if (!inject(XSRF_ENABLED) || req.method === 'GET' || req.method === 'HEAD') {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
try {
|
||||
const locationHref = inject(PlatformLocation).href;
|
||||
const {origin: locationOrigin} = new URL(locationHref);
|
||||
// We can use `new URL` to normalize a relative URL like '//something.com' to
|
||||
// 'https://something.com' in order to make consistent same-origin comparisons.
|
||||
const {origin: requestOrigin} = new URL(req.url, locationOrigin);
|
||||
|
||||
if (locationOrigin !== requestOrigin) {
|
||||
return next(req);
|
||||
}
|
||||
} catch {
|
||||
// Handle invalid URLs gracefully.
|
||||
return next(req);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ ts_project(
|
||||
"//packages/common",
|
||||
"//packages/common/http",
|
||||
"//packages/common/http/testing",
|
||||
"//packages/common/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/private/testing",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT} from '../..';
|
||||
import {MockPlatformLocation} from '@angular/common/testing';
|
||||
import {DOCUMENT, PlatformLocation} from '../..';
|
||||
import {HttpHeaders} from '../src/headers';
|
||||
import {HttpRequest} from '../src/request';
|
||||
import {
|
||||
@@ -47,24 +48,35 @@ describe('HttpXsrfInterceptor', () => {
|
||||
provide: XSRF_ENABLED,
|
||||
useValue: true,
|
||||
},
|
||||
{
|
||||
provide: PlatformLocation,
|
||||
useFactory: () =>
|
||||
new MockPlatformLocation({
|
||||
startUrl: 'http://sub.example.com/',
|
||||
}),
|
||||
},
|
||||
HttpXsrfInterceptor,
|
||||
],
|
||||
});
|
||||
|
||||
interceptor = TestBed.inject(HttpXsrfInterceptor);
|
||||
backend = new HttpClientTestingBackend();
|
||||
});
|
||||
|
||||
it('applies XSRF protection to outgoing requests', () => {
|
||||
interceptor.intercept(new HttpRequest('POST', '/test', {}), backend).subscribe();
|
||||
const req = backend.expectOne('/test');
|
||||
expect(req.request.headers.get('X-XSRF-TOKEN')).toEqual('test');
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('does not apply XSRF protection when request is a GET', () => {
|
||||
interceptor.intercept(new HttpRequest('GET', '/test'), backend).subscribe();
|
||||
const req = backend.expectOne('/test');
|
||||
expect(req.request.headers.has('X-XSRF-TOKEN')).toEqual(false);
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('does not apply XSRF protection when request is a HEAD', () => {
|
||||
interceptor.intercept(new HttpRequest('HEAD', '/test'), backend).subscribe();
|
||||
const req = backend.expectOne('/test');
|
||||
@@ -72,7 +84,7 @@ describe('HttpXsrfInterceptor', () => {
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('does not apply XSRF protection when request is absolute', () => {
|
||||
it('does not apply XSRF protection when request is absolute and cross-origin', () => {
|
||||
interceptor
|
||||
.intercept(new HttpRequest('POST', 'https://example.com/test', {}), backend)
|
||||
.subscribe();
|
||||
@@ -81,13 +93,31 @@ describe('HttpXsrfInterceptor', () => {
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('does not apply XSRF protection when request is protocol relative', () => {
|
||||
it('does not apply XSRF protection when request is protocol relative and cross-origin', () => {
|
||||
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 apply XSRF protection when request is same-origin', () => {
|
||||
interceptor
|
||||
.intercept(new HttpRequest('POST', 'http://sub.example.com/test', {}), backend)
|
||||
.subscribe();
|
||||
const req = backend.expectOne('http://sub.example.com/test');
|
||||
expect(req.request.headers.has('X-XSRF-TOKEN')).toBeTrue();
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('does apply XSRF protection when request is protocol relative and same-origin', () => {
|
||||
interceptor
|
||||
.intercept(new HttpRequest('POST', '//sub.example.com/test', {}), backend)
|
||||
.subscribe();
|
||||
const req = backend.expectOne('//sub.example.com/test');
|
||||
expect(req.request.headers.has('X-XSRF-TOKEN')).toBeTrue();
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('does not overwrite existing header', () => {
|
||||
interceptor
|
||||
.intercept(
|
||||
|
||||
Reference in New Issue
Block a user