From 39e382a756b552d2b7bd3ce2c364daee9d7a0056 Mon Sep 17 00:00:00 2001 From: YooLCD Date: Tue, 7 Apr 2026 17:49:47 +0900 Subject: [PATCH] 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. --- packages/common/http/src/jsonp.ts | 8 +++++++ packages/common/http/test/jsonp_mock.ts | 10 +++++++++ packages/common/http/test/jsonp_spec.ts | 29 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/packages/common/http/src/jsonp.ts b/packages/common/http/src/jsonp.ts index e41bd5401b3..d4b6c72048b 100644 --- a/packages/common/http/src/jsonp.ts +++ b/packages/common/http/src/jsonp.ts @@ -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. diff --git a/packages/common/http/test/jsonp_mock.ts b/packages/common/http/test/jsonp_mock.ts index fa9632196b1..babe8aa772e 100644 --- a/packages/common/http/test/jsonp_mock.ts +++ b/packages/common/http/test/jsonp_mock.ts @@ -25,6 +25,16 @@ export class MockScriptElement { remove() { this.ownerDocument.removeNode(this); } + + private attrs: Record = {}; + + 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 { diff --git a/packages/common/http/test/jsonp_spec.ts b/packages/common/http/test/jsonp_spec.ts index 073a83a13b5..ab442e686d5 100644 --- a/packages/common/http/test/jsonp_spec.ts +++ b/packages/common/http/test/jsonp_spec.ts @@ -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({method: 'GET'}))).toThrowError(