'use strict'; describe('HTML', function() { var ua = window.navigator.userAgent; var isChrome = /Chrome/.test(ua) && !/Edge/.test(ua); var expectHTML; beforeEach(module('ngSanitize')); beforeEach(function() { expectHTML = function(html) { var sanitize; inject(function($sanitize) { sanitize = $sanitize; }); return expect(sanitize(html)); }; }); describe('htmlParser', function() { /* global htmlParser */ var handler, start, text, comment; beforeEach(function() { text = ''; start = null; handler = { start: function(tag, attrs) { start = { tag: tag, attrs: attrs }; // Since different browsers handle newlines differently we trim // so that it is easier to write tests. for (var i = 0, ii = attrs.length; i < ii; i++) { var keyValue = attrs[i]; var key = keyValue.key; var value = keyValue.value; attrs[key] = value.replace(/^\s*/, '').replace(/\s*$/, ''); } }, chars: function(text_) { text += text_; }, end:function(tag) { expect(tag).toEqual(start.tag); }, comment:function(comment_) { comment = comment_; } }; // Trigger the $sanitizer provider to execute, which initializes the `htmlParser` function. inject(function($sanitize) {}); }); it('should not parse comments', function() { htmlParser('', handler); expect(comment).not.toBeDefined(); }); it('should parse basic format', function() { htmlParser('text', handler); expect(start).toEqual({tag:'tag', attrs:{attr:'value'}}); expect(text).toEqual('text'); }); it('should not treat "<" followed by a non-/ or non-letter as a tag', function() { expectHTML('<- text1 text2 <1 text1 text2 <{', handler). toBe('<- text1 text2 <1 text1 text2 <{'); }); it('should accept tag delimiters such as "<" inside real tags', function() { // Assert that the < is part of the text node content, and not part of a tag name. htmlParser('

10 < 100

', handler); expect(text).toEqual(' 10 < 100 '); }); it('should parse newlines in tags', function() { htmlParser('text', handler); expect(start).toEqual({tag:'tag', attrs:{attr:'value'}}); expect(text).toEqual('text'); }); it('should parse newlines in attributes', function() { htmlParser('text', handler); expect(start).toEqual({tag:'tag', attrs:{attr:'\nvalue\n'}}); expect(text).toEqual('text'); }); it('should parse namespace', function() { htmlParser('text', handler); expect(start).toEqual({tag:'ns:t-a-g', attrs:{'ns:a-t-t-r':'\nvalue\n'}}); expect(text).toEqual('text'); }); it('should parse empty value attribute of node', function() { htmlParser('abc', handler); expect(start).toEqual({tag:'test-foo', attrs:{selected:'', value:''}}); expect(text).toEqual('abc'); }); }); // THESE TESTS ARE EXECUTED WITH COMPILED ANGULAR it('should echo html', function() { expectHTML('helloworld.'). toBeOneOf('helloworld.', 'helloworld.'); }); it('should remove script', function() { expectHTML('ac.').toEqual('ac.'); }); it('should remove script that has newline characters', function() { expectHTML('a\n\revil\n\rc.').toEqual('ac.'); }); it('should remove DOCTYPE header', function() { expectHTML('').toEqual(''); expectHTML('').toEqual(''); expectHTML('ac.').toEqual('ac.'); expectHTML('ac.').toEqual('ac.'); }); it('should escape non-start tags', function() { expectHTML('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.'). toBe('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.'); }); it('should remove attrs', function() { expectHTML('a
b
c').toEqual('a
b
c'); }); it('should handle large datasets', function() { // Large is non-trivial to quantify, but handling ~100,000 should be sufficient for most purposes. var largeNumber = 17; // 2^17 = 131,072 var result = '
b
'; // Ideally we would use repeat, but that isn't supported in IE. for (var i = 0; i < largeNumber; i++) { result += result; } expectHTML('a' + result + 'c').toEqual('a' + result + 'c'); }); it('should remove style', function() { expectHTML('ac.').toEqual('ac.'); }); it('should remove style that has newline characters', function() { expectHTML('ac.').toEqual('ac.'); }); it('should remove double nested script', function() { expectHTML('ailc.').toEqual('ailc.'); }); it('should remove unknown names', function() { expectHTML('abc').toEqual('abc'); }); it('should remove unsafe value', function() { expectHTML('').toEqual(''); expectHTML('').toEqual(''); }); it('should handle self closed elements', function() { expectHTML('a
c').toEqual('a
c'); }); it('should handle namespace', function() { expectHTML('abc').toEqual('abc'); }); it('should handle entities', function() { var everything = '
' + '!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ
'; expectHTML(everything).toEqual(everything); }); it('should mangle improper html', function() { // This text is encoded more than a real HTML parser would, but it should render the same. expectHTML('< div rel="" alt=abc dir=\'"\' >text< /div>'). toBe('< div rel="" alt=abc dir=\'"\' >text< /div>'); }); it('should mangle improper html2', function() { // A proper HTML parser would clobber this more in most cases, but it looks reasonable. expectHTML('< div rel="" / >'). toBe('< div rel="" / >'); }); it('should ignore back slash as escape', function() { expectHTML('xxx\\'). toBeOneOf('xxx\\', 'xxx\\'); }); it('should ignore object attributes', function() { expectHTML(':)'). toEqual(':)'); expectHTML(':)'). toEqual(''); }); it('should keep spaces as prefix/postfix', function() { expectHTML(' a ').toEqual(' a '); }); it('should allow multiline strings', function() { expectHTML('\na\n').toEqual(' a '); }); it('should accept tag delimiters such as "<" inside real tags (with nesting)', function() { //this is an integrated version of the 'should accept tag delimiters such as "<" inside real tags' test expectHTML('

10 < 100

') .toEqual('

10 < 100

'); }); it('should accept non-string arguments', function() { expectHTML(null).toBe(''); expectHTML(undefined).toBe(''); expectHTML(42).toBe('42'); expectHTML({}).toBe('[object Object]'); expectHTML([1, 2, 3]).toBe('1,2,3'); expectHTML(true).toBe('true'); expectHTML(false).toBe('false'); }); it('should strip svg elements if not enabled via provider', function() { expectHTML('') .toEqual(''); }); it('should prevent mXSS attacks', function() { expectHTML('CLICKME').toBe('CLICKME'); }); it('should strip html comments', function() { expectHTML('

text1text2

') .toEqual('

text1text2

'); }); describe('clobbered elements', function() { it('should throw on a form with an input named "parentNode"', function() { inject(function($sanitize) { expect(function() { $sanitize('
'); }).toThrowMinErr('$sanitize', 'elclob'); expect(function() { $sanitize('
'); }).toThrowMinErr('$sanitize', 'elclob'); }); }); if (!/Edge\/\d{2,}/.test(window.navigator.userAgent)) { // Skip test on Edge due to a browser bug. it('should throw on a form with an input named "nextSibling"', function() { inject(function($sanitize) { expect(function() { $sanitize('
'); }).toThrowMinErr('$sanitize', 'elclob'); expect(function() { $sanitize('
'); }).toThrowMinErr('$sanitize', 'elclob'); }); }); } }); // See https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449 it('should not allow JavaScript execution when creating inert document', inject(function($sanitize) { $sanitize(''); expect(window.xxx).toBe(undefined); delete window.xxx; })); // See https://github.com/cure53/DOMPurify/releases/tag/0.6.7 it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)', inject(function($sanitize) { var doc = $sanitize('

'); expect(doc).toEqual('

'); })); describe('Custom white-list support', function() { var $sanitizeProvider; beforeEach(module(function(_$sanitizeProvider_) { $sanitizeProvider = _$sanitizeProvider_; $sanitizeProvider.addValidElements(['foo']); $sanitizeProvider.addValidElements({ htmlElements: ['foo-button', 'foo-video'], htmlVoidElements: ['foo-input'], svgElements: ['foo-svg'] }); $sanitizeProvider.addValidAttrs(['foo']); })); it('should allow custom white-listed element', function() { expectHTML('').toEqual(''); expectHTML('').toEqual(''); expectHTML('').toEqual(''); }); it('should allow custom white-listed void element', function() { expectHTML('').toEqual(''); }); it('should allow custom white-listed void element to be used with closing tag', function() { expectHTML('').toEqual(''); }); it('should allow custom white-listed attribute', function() { expectHTML('').toEqual(''); }); it('should ignore custom white-listed SVG element if SVG disabled', function() { expectHTML('').toEqual(''); }); it('should not allow add custom element after service has been instantiated', inject(function($sanitize) { $sanitizeProvider.addValidElements(['bar']); expectHTML('').toEqual(''); })); }); describe('SVG support', function() { beforeEach(module(function($sanitizeProvider) { $sanitizeProvider.enableSvg(true); $sanitizeProvider.addValidElements({ svgElements: ['font-face-uri'] }); })); it('should accept SVG tags', function() { expectHTML('') .toBeOneOf('', '', '', ''); }); it('should not ignore white-listed svg camelCased attributes', function() { expectHTML('') .toBeOneOf('', ''); }); it('should allow custom white-listed SVG element', function() { expectHTML('').toEqual(''); }); it('should sanitize SVG xlink:href attribute values', function() { expectHTML('') .toBeOneOf('', '', ''); expectHTML('') .toBeOneOf('', '', '', ''); }); it('should sanitize SVG xml:base attribute values', function() { expectHTML('') .toEqual(''); expectHTML('') .toEqual(''); }); it('should sanitize unknown namespaced SVG attributes', function() { expectHTML('') .toBeOneOf('', '', ''); expectHTML('') .toBeOneOf('', '', ''); }); it('should not accept SVG animation tags', function() { expectHTML('Click me') .toBeOneOf('Click me', 'Click me', 'Click me'); expectHTML('' + '') .toBeOneOf('', '', '', ''); }); it('should not accept SVG `use` tags', function() { expectHTML('') .toBeOneOf('', '', ''); }); }); describe('htmlSanitizerWriter', function() { /* global htmlSanitizeWriter: false */ var writer, html, uriValidator; beforeEach(function() { html = ''; uriValidator = jasmine.createSpy('uriValidator'); writer = htmlSanitizeWriter({push:function(text) {html += text;}}, uriValidator); }); it('should write basic HTML', function() { writer.chars('before'); writer.start('div', {rel:'123'}, false); writer.chars('in'); writer.end('div'); writer.chars('after'); expect(html).toEqual('before
in
after'); }); it('should escape text nodes', function() { writer.chars('a
&
c'); expect(html).toEqual('a<div>&</div>c'); }); it('should escape IE script', function() { writer.chars('&<>{}'); expect(html).toEqual('&<>{}'); }); it('should escape attributes', function() { writer.start('div', {rel:'!@#$%^&*()_+-={}[]:";\'<>?,./`~ \n\0\r\u0127'}); expect(html).toEqual('
'); }); it('should ignore misformed elements', function() { writer.start('d>i&v', {}); expect(html).toEqual(''); }); it('should ignore unknown attributes', function() { writer.start('div', {unknown:''}); expect(html).toEqual('
'); }); it('should handle surrogate pair', function() { writer.chars(String.fromCharCode(55357, 56374)); expect(html).toEqual('🐶'); }); describe('explicitly disallow', function() { it('should not allow attributes', function() { writer.start('div', {id:'a', name:'a', style:'a'}); expect(html).toEqual('
'); }); it('should not allow tags', function() { function tag(name) { writer.start(name, {}); writer.end(name); } tag('frameset'); tag('frame'); tag('form'); tag('param'); tag('object'); tag('embed'); tag('textarea'); tag('input'); tag('button'); tag('option'); tag('select'); tag('script'); tag('style'); tag('link'); tag('base'); tag('basefont'); expect(html).toEqual(''); }); }); describe('uri validation', function() { it('should call the uri validator', function() { writer.start('a', {href:'someUrl'}, false); expect(uriValidator).toHaveBeenCalledWith('someUrl', false); uriValidator.calls.reset(); writer.start('img', {src:'someImgUrl'}, false); expect(uriValidator).toHaveBeenCalledWith('someImgUrl', true); uriValidator.calls.reset(); writer.start('someTag', {src:'someNonUrl'}, false); expect(uriValidator).not.toHaveBeenCalled(); }); it('should drop non valid uri attributes', function() { uriValidator.and.returnValue(false); writer.start('a', {href:'someUrl'}, false); expect(html).toEqual(''); html = ''; uriValidator.and.returnValue(true); writer.start('a', {href:'someUrl'}, false); expect(html).toEqual(''); }); }); }); describe('uri checking', function() { beforeEach(function() { jasmine.addMatchers({ toBeValidUrl: function() { return { compare: function(actual) { var sanitize; inject(function($sanitize) { sanitize = $sanitize; }); var input = ''; return { pass: sanitize(input) === input }; } }; } }); }); it('should use $$sanitizeUri for a[href] links', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function() { $$sanitizeUri.and.returnValue('someUri'); expectHTML('').toEqual(''); expect($$sanitizeUri).toHaveBeenCalledWith('someUri', false); $$sanitizeUri.and.returnValue('unsafe:someUri'); expectHTML('').toEqual(''); }); }); it('should use $$sanitizeUri for img[src] links', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function() { $$sanitizeUri.and.returnValue('someUri'); expectHTML('').toEqual(''); expect($$sanitizeUri).toHaveBeenCalledWith('someUri', true); $$sanitizeUri.and.returnValue('unsafe:someUri'); expectHTML('').toEqual(''); }); }); it('should be URI', function() { expect('').toBeValidUrl(); expect('http://abc').toBeValidUrl(); expect('HTTP://abc').toBeValidUrl(); expect('https://abc').toBeValidUrl(); expect('HTTPS://abc').toBeValidUrl(); expect('ftp://abc').toBeValidUrl(); expect('FTP://abc').toBeValidUrl(); expect('mailto:me@example.com').toBeValidUrl(); expect('MAILTO:me@example.com').toBeValidUrl(); expect('tel:123-123-1234').toBeValidUrl(); expect('TEL:123-123-1234').toBeValidUrl(); expect('#anchor').toBeValidUrl(); expect('/page1.md').toBeValidUrl(); }); it('should not be URI', function() { // eslint-disable-next-line no-script-url expect('javascript:alert').not.toBeValidUrl(); }); describe('javascript URLs', function() { it('should ignore javascript:', function() { // eslint-disable-next-line no-script-url expect('JavaScript:abc').not.toBeValidUrl(); expect(' \n Java\n Script:abc').not.toBeValidUrl(); expect('http://JavaScript/my.js').toBeValidUrl(); }); it('should ignore dec encoded javascript:', function() { expect('javascript:').not.toBeValidUrl(); expect('javascript:').not.toBeValidUrl(); expect('j avascript:').not.toBeValidUrl(); }); it('should ignore decimal with leading 0 encoded javascript:', function() { expect('javascript:').not.toBeValidUrl(); expect('j avascript:').not.toBeValidUrl(); expect('j avascript:').not.toBeValidUrl(); }); it('should ignore hex encoded javascript:', function() { expect('javascript:').not.toBeValidUrl(); expect('javascript:').not.toBeValidUrl(); expect('j avascript:').not.toBeValidUrl(); }); it('should ignore hex encoded whitespace javascript:', function() { expect('jav ascript:alert();').not.toBeValidUrl(); expect('jav ascript:alert();').not.toBeValidUrl(); expect('jav ascript:alert();').not.toBeValidUrl(); expect('jav\u0000ascript:alert();').not.toBeValidUrl(); expect('java\u0000\u0000script:alert();').not.toBeValidUrl(); expect('  java\u0000\u0000script:alert();').not.toBeValidUrl(); }); }); }); describe('sanitizeText', function() { /* global sanitizeText: false */ it('should escape text', function() { expect(sanitizeText('a
&
c')).toEqual('a<div>&</div>c'); }); }); }); describe('decodeEntities', function() { var handler, text; beforeEach(function() { text = ''; handler = { start: function() {}, chars: function(text_) { text = text_; }, end: function() {}, comment: function() {} }; module('ngSanitize'); }); it('should unescape text', function() { htmlParser('a<div>&</div>c', handler); expect(text).toEqual('a
&
c'); }); it('should preserve whitespace', function() { htmlParser(' a&b ', handler); expect(text).toEqual(' a&b '); }); });