mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Add support for CSP nonces in JsonpClientBackend by injecting the CSP_NONCE token.
This ensures that dynamically created script tags for JSONP requests include the
required nonce attribute to comply with strict Content Security Policies.
(cherry picked from commit 39e382a756)
161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.sonpCallbackContext
|
|
*
|
|
* 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 {DOCUMENT} from '../..';
|
|
import {CSP_NONCE} from '@angular/core';
|
|
import {HttpHeaders} from '../src/headers';
|
|
import {
|
|
JSONP_ERR_HEADERS_NOT_SUPPORTED,
|
|
JSONP_ERR_NO_CALLBACK,
|
|
JSONP_ERR_WRONG_METHOD,
|
|
JSONP_ERR_WRONG_RESPONSE_TYPE,
|
|
JsonpCallbackContext,
|
|
JsonpClientBackend,
|
|
} from '../src/jsonp';
|
|
import {HttpRequest} from '../src/request';
|
|
import {HttpErrorResponse, HttpEventType} from '../src/response';
|
|
import {TestBed} from '@angular/core/testing';
|
|
import {toArray} from 'rxjs/operators';
|
|
|
|
import {MockDocument} from './jsonp_mock';
|
|
|
|
describe('JsonpClientBackend', () => {
|
|
const SAMPLE_REQ = new HttpRequest<never>('JSONP', '/test');
|
|
let home: any;
|
|
let document: MockDocument;
|
|
let backend: JsonpClientBackend;
|
|
|
|
function runOnlyCallback(home: any, data: Object) {
|
|
const keys = Object.keys(home);
|
|
expect(keys.length).toBe(1);
|
|
const callback = home[keys[0]];
|
|
callback(data);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
const mockDoc = new MockDocument();
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
JsonpClientBackend,
|
|
{provide: JsonpCallbackContext, useValue: {}},
|
|
{provide: DOCUMENT, useValue: mockDoc},
|
|
{provide: CSP_NONCE, useValue: null},
|
|
],
|
|
});
|
|
backend = TestBed.inject(JsonpClientBackend);
|
|
home = TestBed.inject(JsonpCallbackContext);
|
|
document = mockDoc;
|
|
});
|
|
|
|
it('handles a basic request', (done) => {
|
|
backend
|
|
.handle(SAMPLE_REQ)
|
|
.pipe(toArray())
|
|
.subscribe((events) => {
|
|
expect(events.map((event) => event.type)).toEqual([
|
|
HttpEventType.Sent,
|
|
HttpEventType.Response,
|
|
]);
|
|
done();
|
|
});
|
|
runOnlyCallback(home, {data: 'This is a test'});
|
|
document.mockLoad();
|
|
});
|
|
// Issue #39496
|
|
it('handles a request with callback call wrapped in promise', (done) => {
|
|
backend.handle(SAMPLE_REQ).subscribe({complete: done});
|
|
queueMicrotask(() => {
|
|
runOnlyCallback(home, {data: 'This is a test'});
|
|
});
|
|
document.mockLoad();
|
|
});
|
|
it('handles an error response properly', (done) => {
|
|
const error = new Error('This is a test error');
|
|
backend
|
|
.handle(SAMPLE_REQ)
|
|
.pipe(toArray())
|
|
.subscribe(undefined, (err: HttpErrorResponse) => {
|
|
expect(err.status).toBe(0);
|
|
expect(err.error).toBe(error);
|
|
done();
|
|
});
|
|
document.mockError(error);
|
|
});
|
|
it('prevents the script from executing when the request is cancelled', () => {
|
|
const sub = backend.handle(SAMPLE_REQ).subscribe();
|
|
expect(Object.keys(home).length).toBe(1);
|
|
const keys = Object.keys(home);
|
|
const spy = jasmine.createSpy('spy', home[keys[0]]);
|
|
|
|
sub.unsubscribe();
|
|
document.mockLoad();
|
|
expect(Object.keys(home).length).toBe(0);
|
|
expect(spy).not.toHaveBeenCalled();
|
|
// The script element should have been transferred to a different document to prevent it from
|
|
// executing.
|
|
expect(document.mock!.ownerDocument).not.toEqual(document);
|
|
});
|
|
describe('CSP nonce', () => {
|
|
it('sets nonce attribute on script element when CSP_NONCE token is provided', (done) => {
|
|
TestBed.resetTestingModule();
|
|
const mockDoc = new MockDocument();
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
JsonpClientBackend,
|
|
{provide: JsonpCallbackContext, useValue: {}},
|
|
{provide: DOCUMENT, useValue: mockDoc},
|
|
{provide: CSP_NONCE, useValue: 'test-nonce-123'},
|
|
],
|
|
});
|
|
const nonceBackend = TestBed.inject(JsonpClientBackend);
|
|
nonceBackend.handle(SAMPLE_REQ).subscribe();
|
|
|
|
expect(mockDoc.mock!.getAttribute('nonce')).toBe('test-nonce-123');
|
|
done();
|
|
});
|
|
|
|
it('does not set nonce attribute when CSP_NONCE token is not provided', (done) => {
|
|
backend.handle(SAMPLE_REQ).subscribe();
|
|
|
|
expect(document.mock!.getAttribute('nonce')).toBeNull();
|
|
done();
|
|
});
|
|
});
|
|
|
|
describe('throws an error', () => {
|
|
it('when request method is not JSONP', () =>
|
|
expect(() => backend.handle(SAMPLE_REQ.clone<never>({method: 'GET'}))).toThrowError(
|
|
`NG02810: ${JSONP_ERR_WRONG_METHOD}`,
|
|
));
|
|
it('when response type is not json', () =>
|
|
expect(() =>
|
|
backend.handle(
|
|
SAMPLE_REQ.clone<never>({
|
|
responseType: 'text',
|
|
}),
|
|
),
|
|
).toThrowError(`NG02811: ${JSONP_ERR_WRONG_RESPONSE_TYPE}`));
|
|
it('when headers are set in request', () =>
|
|
expect(() =>
|
|
backend.handle(
|
|
SAMPLE_REQ.clone<never>({
|
|
headers: new HttpHeaders({'Content-Type': 'application/json'}),
|
|
}),
|
|
),
|
|
).toThrowError(`NG02812: ${JSONP_ERR_HEADERS_NOT_SUPPORTED}`));
|
|
it('when callback is never called', (done) => {
|
|
backend.handle(SAMPLE_REQ).subscribe(undefined, (err: HttpErrorResponse) => {
|
|
expect(err.status).toBe(0);
|
|
expect(err.error instanceof Error).toEqual(true);
|
|
expect(err.error.message).toEqual(JSONP_ERR_NO_CALLBACK);
|
|
done();
|
|
});
|
|
document.mockLoad();
|
|
});
|
|
});
|
|
});
|