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.
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[];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user