mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fa940e1f4d
Prevents JSONP requests from using URLs with unsupported protocols for improved security. Fixes angular#68832
160 lines
5.0 KiB
TypeScript
160 lines
5.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.sonpCallbackContext
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
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,
|
|
} from '../src/jsonp';
|
|
import {HttpRequest} from '../src/request';
|
|
import {HttpErrorResponse, HttpEventType} from '../src/response';
|
|
import {toArray} from 'rxjs/operators';
|
|
|
|
import {MockDocument} from './jsonp_mock';
|
|
|
|
describe('JsonpClientBackend', () => {
|
|
const SAMPLE_REQ = new HttpRequest<never>('JSONP', 'https://example.com/test');
|
|
let home: any;
|
|
let document: MockDocument;
|
|
let backend: JsonpClientBackend;
|
|
|
|
function runOnlyCallback(home: any, data: Object) {
|
|
const keys = Object.keys(home);
|
|
expect(keys.length).toBe(1);
|
|
const callback = home[keys[0]];
|
|
callback(data);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
home = {};
|
|
document = new MockDocument();
|
|
backend = new JsonpClientBackend(home, document);
|
|
});
|
|
|
|
it('handles a basic request', (done) => {
|
|
backend
|
|
.handle(SAMPLE_REQ)
|
|
.pipe(toArray())
|
|
.subscribe((events) => {
|
|
expect(events.map((event) => event.type)).toEqual([
|
|
HttpEventType.Sent,
|
|
HttpEventType.Response,
|
|
]);
|
|
done();
|
|
});
|
|
runOnlyCallback(home, {data: 'This is a test'});
|
|
document.mockLoad();
|
|
});
|
|
// Issue #39496
|
|
it('handles a request with callback call wrapped in promise', (done) => {
|
|
backend.handle(SAMPLE_REQ).subscribe({complete: done});
|
|
queueMicrotask(() => {
|
|
runOnlyCallback(home, {data: 'This is a test'});
|
|
});
|
|
document.mockLoad();
|
|
});
|
|
it('handles an error response properly', (done) => {
|
|
const error = new Error('This is a test error');
|
|
backend
|
|
.handle(SAMPLE_REQ)
|
|
.pipe(toArray())
|
|
.subscribe(undefined, (err: HttpErrorResponse) => {
|
|
expect(err.status).toBe(0);
|
|
expect(err.error).toBe(error);
|
|
done();
|
|
});
|
|
document.mockError(error);
|
|
});
|
|
it('prevents the script from executing when the request is cancelled', () => {
|
|
const sub = backend.handle(SAMPLE_REQ).subscribe();
|
|
expect(Object.keys(home).length).toBe(1);
|
|
const keys = Object.keys(home);
|
|
const spy = jasmine.createSpy('spy', home[keys[0]]);
|
|
|
|
sub.unsubscribe();
|
|
document.mockLoad();
|
|
expect(Object.keys(home).length).toBe(0);
|
|
expect(spy).not.toHaveBeenCalled();
|
|
// The script element should have been transferred to a different document to prevent it from
|
|
// 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(
|
|
`NG02810: ${JSONP_ERR_WRONG_METHOD}`,
|
|
));
|
|
it('when response type is not json', () =>
|
|
expect(() =>
|
|
backend.handle(
|
|
SAMPLE_REQ.clone<never>({
|
|
responseType: 'text',
|
|
}),
|
|
),
|
|
).toThrowError(`NG02811: ${JSONP_ERR_WRONG_RESPONSE_TYPE}`));
|
|
it('when headers are set in request', () =>
|
|
expect(() =>
|
|
backend.handle(
|
|
SAMPLE_REQ.clone<never>({
|
|
headers: new HttpHeaders({'Content-Type': 'application/json'}),
|
|
}),
|
|
),
|
|
).toThrowError(`NG02812: ${JSONP_ERR_HEADERS_NOT_SUPPORTED}`));
|
|
it('when callback is never called', (done) => {
|
|
backend.handle(SAMPLE_REQ).subscribe(undefined, (err: HttpErrorResponse) => {
|
|
expect(err.status).toBe(0);
|
|
expect(err.error instanceof Error).toEqual(true);
|
|
expect(err.error.message).toEqual(JSONP_ERR_NO_CALLBACK);
|
|
done();
|
|
});
|
|
document.mockLoad();
|
|
});
|
|
});
|
|
});
|