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

Prevents JSONP requests from using URLs with unsupported protocols
for improved security.
This commit is contained in:
Jaime Burgos
2026-06-05 16:25:46 -05:00
committed by GitHub
parent 35510746b7
commit a810a319d1
5 changed files with 59 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[];
});
}
+2
View File
@@ -35,4 +35,6 @@ export const enum RuntimeErrorCode {
REFERRER_NOT_SUPPORTED_WITH_XHR = 2821,
INVALID_TIMEOUT_VALUE = 2822,
REFERRER_POLICY_NOT_SUPPORTED_WITH_XHR = 2823,
JSONP_UNSAFE_URL = 2826,
}
+12
View File
@@ -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<HttpEvent<any>>((observer: Observer<HttpEvent<any>>) => {
// 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);
}
}
/**
+40 -1
View File
@@ -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<never>('JSONP', '/test');
const SAMPLE_REQ = new HttpRequest<never>('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<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(