angular/packages/common/http/test/xsrf_spec.ts
Alan Agius 20474d3f0f 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.

(cherry picked from commit 0659d11c85)
2025-12-03 12:19:46 +01:00

201 lines
6.2 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {MockPlatformLocation} from '@angular/common/testing';
import {DOCUMENT, PlatformLocation} from '../..';
import {HttpHeaders} from '../src/headers';
import {HttpRequest} from '../src/request';
import {
HttpXsrfCookieExtractor,
HttpXsrfInterceptor,
HttpXsrfTokenExtractor,
XSRF_ENABLED,
XSRF_HEADER_NAME,
} from '../src/xsrf';
import {HttpClientTestingBackend} from '../testing/src/backend';
import {TestBed} from '@angular/core/testing';
class SampleTokenExtractor extends HttpXsrfTokenExtractor {
constructor(private token: string | null) {
super();
}
override getToken(): string | null {
return this.token;
}
}
describe('HttpXsrfInterceptor', () => {
let backend: HttpClientTestingBackend;
let interceptor: HttpXsrfInterceptor;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: HttpXsrfTokenExtractor,
useValue: new SampleTokenExtractor('test'),
},
{
provide: XSRF_HEADER_NAME,
useValue: 'X-XSRF-TOKEN',
},
{
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');
expect(req.request.headers.has('X-XSRF-TOKEN')).toEqual(false);
req.flush({});
});
it('does not apply XSRF protection when request is absolute and cross-origin', () => {
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 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(
new HttpRequest(
'POST',
'/test',
{},
{headers: new HttpHeaders().set('X-XSRF-TOKEN', 'blah')},
),
backend,
)
.subscribe();
const req = backend.expectOne('/test');
expect(req.request.headers.get('X-XSRF-TOKEN')).toEqual('blah');
req.flush({});
});
it('does not set the header for a null token', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
providers: [
{
provide: HttpXsrfTokenExtractor,
useValue: new SampleTokenExtractor(null),
},
{
provide: XSRF_HEADER_NAME,
useValue: 'X-XSRF-TOKEN',
},
{
provide: XSRF_ENABLED,
useValue: true,
},
HttpXsrfInterceptor,
],
});
interceptor = TestBed.inject(HttpXsrfInterceptor);
interceptor.intercept(new HttpRequest('POST', '/test', {}), backend).subscribe();
const req = backend.expectOne('/test');
expect(req.request.headers.has('X-XSRF-TOKEN')).toEqual(false);
req.flush({});
});
afterEach(() => {
backend.verify();
});
});
describe('HttpXsrfCookieExtractor', () => {
let document: {[key: string]: string};
let extractor: HttpXsrfCookieExtractor;
beforeEach(() => {
document = {
cookie: 'XSRF-TOKEN=test',
};
TestBed.configureTestingModule({
providers: [
{
provide: DOCUMENT,
useValue: document,
},
],
});
extractor = TestBed.inject(HttpXsrfCookieExtractor);
});
it('parses the cookie from document.cookie', () => {
expect(extractor.getToken()).toEqual('test');
});
it('does not re-parse if document.cookie has not changed', () => {
expect(extractor.getToken()).toEqual('test');
expect(extractor.getToken()).toEqual('test');
expect(getParseCount(extractor)).toEqual(1);
});
it('re-parses if document.cookie changes', () => {
expect(extractor.getToken()).toEqual('test');
document['cookie'] = 'XSRF-TOKEN=blah';
expect(extractor.getToken()).toEqual('blah');
expect(getParseCount(extractor)).toEqual(2);
});
});
function getParseCount(extractor: HttpXsrfCookieExtractor): number {
return (extractor as any).parseCount;
}