fix(http): add CSP nonce support to JsonpClientBackend

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.
This commit is contained in:
YooLCD 2026-04-07 17:49:47 +09:00 committed by Kirill Cherkashin
parent df9eed4ff1
commit 39e382a756
3 changed files with 47 additions and 0 deletions

View file

@ -8,6 +8,7 @@
import {DOCUMENT} from '../../index';
import {
CSP_NONCE,
EnvironmentInjector,
Inject,
inject,
@ -94,6 +95,7 @@ export class JsonpClientBackend implements HttpBackend {
* A resolved promise that can be used to schedule microtasks in the event handlers.
*/
private readonly resolvedPromise = Promise.resolve();
private readonly nonce = inject(CSP_NONCE, {optional: true});
constructor(
private callbackMap: JsonpCallbackContext,
@ -149,6 +151,12 @@ export class JsonpClientBackend implements HttpBackend {
const node = this.document.createElement('script');
node.src = url;
// Set the nonce for Content Security Policy compatibility. Without this,
// JSONP requests will be blocked by strict-dynamic CSP policies.
if (this.nonce) {
node.setAttribute('nonce', this.nonce);
}
// A JSONP request requires waiting for multiple callbacks. These variables
// are closed over and track state across those callbacks.

View file

@ -25,6 +25,16 @@ export class MockScriptElement {
remove() {
this.ownerDocument.removeNode(this);
}
private attrs: Record<string, string> = {};
setAttribute(name: string, value: string): void {
this.attrs[name] = value;
}
getAttribute(name: string): string | null {
return this.attrs.hasOwnProperty(name) ? this.attrs[name] : null;
}
}
export class MockDocument {

View file

@ -7,6 +7,7 @@
*/
import {DOCUMENT} from '../..';
import {CSP_NONCE} from '@angular/core';
import {HttpHeaders} from '../src/headers';
import {
JSONP_ERR_HEADERS_NOT_SUPPORTED,
@ -43,6 +44,7 @@ describe('JsonpClientBackend', () => {
JsonpClientBackend,
{provide: JsonpCallbackContext, useValue: {}},
{provide: DOCUMENT, useValue: mockDoc},
{provide: CSP_NONCE, useValue: null},
],
});
backend = TestBed.inject(JsonpClientBackend);
@ -98,6 +100,33 @@ describe('JsonpClientBackend', () => {
// 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(