From b91c3464acb0efa56b69da23f01f8d7a3f08b481 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:33:17 -0500 Subject: [PATCH] fix(http): Rejects non-HTTP(S) URLs in JSONP requests Prevents JSONP requests from using URLs with unsupported protocols for improved security. Fixes #68832 (cherry picked from commit 231eff19a1707917a8228b4b06d680b5f49e02a0) --- goldens/public-api/common/http/errors.api.md | 2 + .../playground/src/jsonp/app/jsonp_comp.ts | 4 +- packages/common/http/src/errors.ts | 1 + packages/common/http/src/jsonp.ts | 12 ++++++ packages/common/http/test/jsonp_spec.ts | 41 ++++++++++++++++++- 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/goldens/public-api/common/http/errors.api.md b/goldens/public-api/common/http/errors.api.md index b5fe55db0c90..1381e55d3b82 100644 --- a/goldens/public-api/common/http/errors.api.md +++ b/goldens/public-api/common/http/errors.api.md @@ -29,6 +29,8 @@ export const enum RuntimeErrorCode { // (undocumented) JSONP_HEADERS_NOT_SUPPORTED = 2812, // (undocumented) + JSONP_UNSAFE_URL = 2826, + // (undocumented) JSONP_WRONG_METHOD = 2810, // (undocumented) JSONP_WRONG_RESPONSE_TYPE = 2811, diff --git a/modules/playground/src/jsonp/app/jsonp_comp.ts b/modules/playground/src/jsonp/app/jsonp_comp.ts index d85a38a324c3..8ed00aaf2fab 100644 --- a/modules/playground/src/jsonp/app/jsonp_comp.ts +++ b/modules/playground/src/jsonp/app/jsonp_comp.ts @@ -28,7 +28,9 @@ export class JsonpCmp { people: Person[] = []; constructor(http: HttpClient) { - http.jsonp('./people.json', 'callback').subscribe((res: unknown) => { + const peopleUrl = new URL('./people.json', window.location.href).toString(); + + http.jsonp(peopleUrl, 'callback').subscribe((res: unknown) => { this.people = res as Person[]; }); } diff --git a/packages/common/http/src/errors.ts b/packages/common/http/src/errors.ts index 9266c6e8aeef..686946fce9d9 100644 --- a/packages/common/http/src/errors.ts +++ b/packages/common/http/src/errors.ts @@ -38,4 +38,5 @@ export const enum RuntimeErrorCode { FETCH_UPLOAD_PROGRESS_NOT_SUPPORTED = 2824, FETCH_RESPONSE_BODY_TOO_LARGE = 2825, + JSONP_UNSAFE_URL = 2826, } diff --git a/packages/common/http/src/jsonp.ts b/packages/common/http/src/jsonp.ts index d4b6c72048b1..b62a16a05337 100644 --- a/packages/common/http/src/jsonp.ts +++ b/packages/common/http/src/jsonp.ts @@ -55,6 +55,10 @@ export const JSONP_ERR_WRONG_RESPONSE_TYPE = 'JSONP requests must use Json respo // headers set export const JSONP_ERR_HEADERS_NOT_SUPPORTED = 'JSONP requests do not support headers.'; +// Error text given when a JSONP request URL is not absolute HTTP(S). +export const JSONP_ERR_UNSAFE_URL = + 'JSONP requests only support absolute URLs with HTTP(S) protocols.'; + /** * DI token/abstract type representing a map of JSONP callbacks. * @@ -139,6 +143,10 @@ export class JsonpClientBackend implements HttpBackend { ); } + if (!this.isAllowedJsonpUrl(req.urlWithParams)) { + throw new RuntimeError(RuntimeErrorCode.JSONP_UNSAFE_URL, ngDevMode && JSONP_ERR_UNSAFE_URL); + } + // Everything else happens inside the Observable boundary. return new Observable>((observer: Observer>) => { // The first step to make a request is to generate the callback name, and replace the @@ -282,6 +290,10 @@ export class JsonpClientBackend implements HttpBackend { foreignDocument.adoptNode(script); } + + private isAllowedJsonpUrl(url: string): boolean { + return /^https?:\/\//i.test(url); + } } /** diff --git a/packages/common/http/test/jsonp_spec.ts b/packages/common/http/test/jsonp_spec.ts index ab442e686d5f..0c668c7520c4 100644 --- a/packages/common/http/test/jsonp_spec.ts +++ b/packages/common/http/test/jsonp_spec.ts @@ -12,6 +12,7 @@ import {HttpHeaders} from '../src/headers'; import { JSONP_ERR_HEADERS_NOT_SUPPORTED, JSONP_ERR_NO_CALLBACK, + JSONP_ERR_UNSAFE_URL, JSONP_ERR_WRONG_METHOD, JSONP_ERR_WRONG_RESPONSE_TYPE, JsonpCallbackContext, @@ -25,7 +26,7 @@ import {toArray} from 'rxjs/operators'; import {MockDocument} from './jsonp_mock'; describe('JsonpClientBackend', () => { - const SAMPLE_REQ = new HttpRequest('JSONP', '/test'); + const SAMPLE_REQ = new HttpRequest('JSONP', 'https://example.com/test'); let home: any; let document: MockDocument; let backend: JsonpClientBackend; @@ -127,6 +128,44 @@ describe('JsonpClientBackend', () => { }); }); + describe('URL protocols', () => { + it('allows absolute HTTP(S) URLs', () => { + const urls = [ + 'http://example.com/test', + 'https://example.com/test', + 'HTTP://example.com/test', + ]; + + for (const url of urls) { + const subscription = backend.handle(SAMPLE_REQ.clone({url})).subscribe(); + + subscription.unsubscribe(); + } + }); + + it('rejects URLs without absolute HTTP(S) protocols before creating a script element', () => { + const urls = [ + '//example.com/test', + '/test', + 'test', + 'data:text/javascript,alert(1)', + 'blob:https://example.com/jsonp', + 'javascript:alert(1)', + 'file:///tmp/jsonp.js', + 'filesystem:https://example.com/temporary/jsonp.js', + 'ftp://example.com/jsonp.js', + 'custom-scheme://example.com/jsonp.js', + ]; + + for (const url of urls) { + expect(() => backend.handle(SAMPLE_REQ.clone({url}))).toThrowError( + `NG02826: ${JSONP_ERR_UNSAFE_URL}`, + ); + expect(document.mock).toBeUndefined(); + } + }); + }); + describe('throws an error', () => { it('when request method is not JSONP', () => expect(() => backend.handle(SAMPLE_REQ.clone({method: 'GET'}))).toThrowError(