fix(http): Rejects non-HTTP(S) URLs in JSONP requests

Prevents JSONP requests from using URLs with unsupported protocols
for improved security.

Fixes  angular#68832
This commit is contained in:
SkyZeroZx
2026-06-01 17:06:54 -05:00
committed by Andrew Scott
parent d65a5f457b
commit fa940e1f4d
5 changed files with 57 additions and 2 deletions
@@ -25,6 +25,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,
@@ -27,7 +27,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[];
});
}
+1
View File
@@ -34,4 +34,5 @@ export const enum RuntimeErrorCode {
INTEGRITY_NOT_SUPPORTED_WITH_XHR = 2820,
REFERRER_NOT_SUPPORTED_WITH_XHR = 2821,
INVALID_TIMEOUT_VALUE = 2822,
JSONP_UNSAFE_URL = 2826,
}
+12
View File
@@ -54,6 +54,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.
*
@@ -137,6 +141,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<HttpEvent<any>>((observer: Observer<HttpEvent<any>>) => {
// The first step to make a request is to generate the callback name, and replace the
@@ -274,6 +282,10 @@ export class JsonpClientBackend implements HttpBackend {
foreignDocument.adoptNode(script);
}
private isAllowedJsonpUrl(url: string): boolean {
return /^https?:\/\//i.test(url);
}
}
/**
+39 -1
View File
@@ -10,6 +10,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,
JsonpClientBackend,
@@ -21,7 +22,7 @@ import {toArray} from 'rxjs/operators';
import {MockDocument} from './jsonp_mock';
describe('JsonpClientBackend', () => {
const SAMPLE_REQ = new HttpRequest<never>('JSONP', '/test');
const SAMPLE_REQ = new HttpRequest<never>('JSONP', 'https://example.com/test');
let home: any;
let document: MockDocument;
let backend: JsonpClientBackend;
@@ -87,6 +88,43 @@ describe('JsonpClientBackend', () => {
// executing.
expect(document.mock!.ownerDocument).not.toEqual(document);
});
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<never>({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<never>({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<never>({method: 'GET'}))).toThrowError(