mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
@@ -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[];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user