This is based on Angular 1's implementation, parsing an HTML document into an inert DOM Document implementation, and then serializing only specifically whitelisted elements. It currently does not support SVG sanitization, all SVG elements are rejected. If available, the sanitizer uses the `<template>` HTML element as an inert container. Sanitization works client and server-side. Reviewers: rjamet, tbosch , molnarg , koto Differential Revision: https://reviews.angular.io/D108
93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
import * as t from '@angular/core/testing/testing_internal';
|
|
import {browserDetection} from '@angular/platform-browser/testing';
|
|
|
|
import {getDOM} from '../../src/dom/dom_adapter';
|
|
import {sanitizeHtml} from '../../src/security/html_sanitizer';
|
|
|
|
export function main() {
|
|
t.describe('HTML sanitizer', () => {
|
|
let originalLog: (msg: any) => any = null;
|
|
let logMsgs: string[];
|
|
|
|
t.beforeEach(() => {
|
|
logMsgs = [];
|
|
originalLog = getDOM().log; // Monkey patch DOM.log.
|
|
getDOM().log = (msg) => logMsgs.push(msg);
|
|
});
|
|
t.afterEach(() => { getDOM().log = originalLog; });
|
|
|
|
t.it('serializes nested structures', () => {
|
|
t.expect(sanitizeHtml('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>'))
|
|
.toEqual('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>');
|
|
t.expect(logMsgs).toEqual([]);
|
|
});
|
|
t.it('serializes self closing elements', () => {
|
|
t.expect(sanitizeHtml('<p>Hello <br> World</p>')).toEqual('<p>Hello <br> World</p>');
|
|
});
|
|
t.it('supports namespaced elements',
|
|
() => { t.expect(sanitizeHtml('a<my:hr/><my:div>b</my:div>c')).toEqual('abc'); });
|
|
t.it('supports namespaced attributes', () => {
|
|
t.expect(sanitizeHtml('<a xlink:href="something">t</a>'))
|
|
.toEqual('<a xlink:href="something">t</a>');
|
|
t.expect(sanitizeHtml('<a xlink:evil="something">t</a>')).toEqual('<a>t</a>');
|
|
t.expect(sanitizeHtml('<a xlink:href="javascript:foo()">t</a>'))
|
|
.toEqual('<a xlink:href="unsafe:javascript:foo()">t</a>');
|
|
});
|
|
|
|
t.it('supports sanitizing plain text',
|
|
() => { t.expect(sanitizeHtml('Hello, World')).toEqual('Hello, World'); });
|
|
t.it('ignores non-element, non-attribute nodes', () => {
|
|
t.expect(sanitizeHtml('<!-- comments? -->no.')).toEqual('no.');
|
|
t.expect(sanitizeHtml('<?pi nodes?>no.')).toEqual('no.');
|
|
t.expect(logMsgs.join('\n')).toMatch(/HTML contents were removed during sanitization/);
|
|
});
|
|
t.it('escapes entities', () => {
|
|
t.expect(sanitizeHtml('<p>Hello < World</p>')).toEqual('<p>Hello < World</p>');
|
|
t.expect(sanitizeHtml('<p>Hello < World</p>')).toEqual('<p>Hello < World</p>');
|
|
t.expect(sanitizeHtml('<p alt="% & " !">Hello</p>'))
|
|
.toEqual('<p alt="% & " !">Hello</p>'); // NB: quote encoded as ASCII ".
|
|
});
|
|
t.describe('should strip dangerous elements', () => {
|
|
let dangerousTags = [
|
|
'frameset',
|
|
'form',
|
|
'param',
|
|
'object',
|
|
'embed',
|
|
'textarea',
|
|
'input',
|
|
'button',
|
|
'option',
|
|
'select',
|
|
'script',
|
|
'style',
|
|
'link',
|
|
'base',
|
|
'basefont'
|
|
];
|
|
|
|
for (let tag of dangerousTags) {
|
|
t.it(`${tag}`,
|
|
() => { t.expect(sanitizeHtml(`<${tag}>evil!</${tag}>`)).toEqual('evil!'); });
|
|
}
|
|
t.it(`swallows frame entirely`,
|
|
() => { t.expect(sanitizeHtml(`<frame>evil!</frame>`)).not.toContain('<frame>'); });
|
|
});
|
|
t.describe('should strip dangerous attributes', () => {
|
|
let dangerousAttrs = ['id', 'name', 'style'];
|
|
|
|
for (let attr of dangerousAttrs) {
|
|
t.it(`${attr}`,
|
|
() => { t.expect(sanitizeHtml(`<a ${attr}="x">evil!</a>`)).toEqual('<a>evil!</a>'); });
|
|
}
|
|
});
|
|
|
|
if (browserDetection.isWebkit) {
|
|
t.it('should prevent mXSS attacks', function() {
|
|
t.expect(sanitizeHtml('<a href=" javascript:alert(1)">CLICKME</a>'))
|
|
.toEqual('<a href="unsafe:javascript:alert(1)">CLICKME</a>');
|
|
});
|
|
}
|
|
});
|
|
}
|