// Copyright 2013 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('goog.labs.html.SanitizerTest'); goog.require('goog.html.SafeUrl'); goog.require('goog.labs.html.Sanitizer'); goog.require('goog.string'); goog.require('goog.string.Const'); goog.require('goog.testing.jsunit'); goog.setTestOnly('goog.labs.html.SanitizerTest'); var JENNYS_PHONE_NUMBER = goog.html.SafeUrl.fromConstant( goog.string.Const.from('tel:867-5309')); var sanitizer = new goog.labs.html.Sanitizer() .allowElements( 'a', 'b', 'i', 'p', 'font', 'hr', 'br', 'span', 'ol', 'ul', 'li', 'table', 'tr', 'td', 'th', 'tbody', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'html', 'head', 'body', 'title' ) // allow unfiltered title attributes, and .allowAttributes('*', 'title') // specific dir values. .allowAttributes( '*', 'dir', function(dir) { return dir === 'ltr' || dir === 'rtl' ? dir : null; }) .allowAttributes( // Specifically on <a> elements, 'a', // allow an href but verify and rewrite, and 'href', function(href) { if (href === 'tel:867-5309') { return JENNYS_PHONE_NUMBER; } // Missing anchor is an intentional error. return /https?:\/\/google\.[a-z]{2,3}\/search\?/.test(String(href)) ? href : null; }) .allowAttributes( 'a', // mask the generic title handler for no good reason. 'title', function(title) { return '<' + title + '>'; }); function run(input, golden, desc) { var actual = sanitizer.sanitize(input); assertEquals(desc, golden, actual); } function testEmptyString() { run('', '', 'Empty string'); } function testHelloWorld() { run('Hello, <b>World</b>!', 'Hello, <b>World</b>!', 'Hello World'); } function testNoEndTag() { run('<i>Hello, <b>World!', '<i>Hello, <b>World!</b></i>', 'Hello World no end tag'); } function testUnclosedTags() { run('<html><head><title>Hello, <<World>>!</TITLE>' + '</head><body><p>Hello,<Br><<World>>!', '<html><head><title>Hello, <<World>>!</title>' + '</head><body><p>Hello,<br><>!</p></body></html>', 'RCDATA content, different case, unclosed tags'); } function testListInList() { run('<ul><li>foo</li><ul><li>bar</li></ul></ul>', '<ul><li>foo</li><li><ul><li>bar</li></ul></li></ul>', 'list in list directly'); } function testHeaders() { run('<h1>header</h1>body' + '<H2>sub-header</h3>sub-body' + '<h3>sub-sub-</hr>header<hr></hr>sub-sub-body</H4></h2>', '<h1>header</h1>body' + '<h2>sub-header</h2>sub-body' + '<h3>sub-sub-header</h3><hr>sub-sub-body', 'headers'); } function testListNesting() { run('<ul><li><ul><li>foo</li></li><ul><li>bar', '<ul><li><ul><li>foo</li><li><ul><li>bar</li></ul></li></ul></li></ul>', 'list nesting'); } function testTableNesting() { run('<table><tbody><tr><td>foo</td><table><tbody><tr><th>bar</table></table>', '<table><tbody><tr><td>foo</td><td>' + '<table><tbody><tr><th>bar</th></tr></tbody></table>' + '</td></tr></tbody></table>', 'table nesting'); } function testNestingLimit() { run(goog.string.repeat('<span>', 264) + goog.string.repeat('</span>', 264), goog.string.repeat('<span>', 256) + goog.string.repeat('</span>', 256), '264 open spans'); } function testTableScopes() { run('<html><head></head><body><p>Hi</p><p>How are you</p>\n' + '<p><table><tbody><tr>' + '<td><b><font><font><p>Cell</b></font></font></p>\n</td>' + '<td><b><font><font><p>Cell</b></font></font></p>\n</td>' + '</tr></tbody></table></p>\n' + '<p>x</p></body></html>', '<html><head></head><body><p>Hi</p><p>How are you</p>\n' + '<p><table><tbody><tr>' + '<td><b><font><font></font></font></b><p>Cell</p>\n</td>' + // The close </p> tag does not close the whole table. + '<td><b><font><font></font></font></b><p>Cell</p>\n</td>' + '</tr></tbody></table></p>\n' + '<p>x</p></body></html>', 'Table Scopes'); } function testConcatSafe() { run('<<applet>script<applet>>alert(1337)<<!-- -->/script<?...?>>', '<script>alert(1337)</script>', 'Concat safe'); } function testPrototypeMembersDoNotInfectTables() { // Constructor is all lower-case so will survive tag name // normalization. run('<constructor>Foo</constructor>', 'Foo', 'Object.prototype members'); } function testGenericAttributesAllowed() { run('<span title=howdy></span>', '<span title="howdy"></span>', 'generic attrs allowed'); } function testValueWhitelisting() { run('<span dir=\'ltr\'>LTR</span><span dir=\'evil\'>Evil</span>', '<span dir="ltr">LTR</span><span>Evil</span>', 'value whitelisted'); } function testAttributeNormalization() { run('<a href="http://google.com/search?q=tests suxor&hl=en">Click</a>', '<a href="http://google.com/search?q=tests%20suxor&hl=en">Click</a>', 'URL normalized'); } function testNaiveAttributeRewriterCaught() { run('<a href="javascript:http://google.com/ alert(1337)">sneaky</a>', '<a>sneaky</a>', 'Safety net saves naive attribute rewriters'); } function testSafeUrlFromAttributeRewriter() { run('<a href="tel:867-5309">Jenny</a>', '<a href="tel:867-5309">Jenny</a>', 'Attribute rewriter escapes safety checks via SafeURL'); } function testTagSpecificityOfAttributeFiltering() { run('<img href="http://google.com/search?q=tests+suxor">', '<img>', 'href blocked on img'); } function testTagSpecificAttributeFiltering() { run('<a href="http://google.evil.com/search?q=tests suxor">Unclicky</a>', '<a>Unclicky</a>', 'bad href value blocked'); } function testNonWhitelistFunctionsNotCalled() { var called = false; Object.prototype.dontcallme = function() { called = true; return 'dontcallme was called despite being on the prototype'; }; try { run('<span dontcallme="I\'ll call you">Lorem Ipsum', '<span>Lorem Ipsum</span>', 'non white-list fn not called'); } finally { delete Object.prototype.dontcallme; } assertFalse('Object.prototype.dontcallme should not have been called', called); } function testQuotesInAttributeValue() { run('<span tItlE =\n\'Quoth the raven, "Nevermore"\'>Lorem Ipsum', '<span title="Quoth the raven, "Nevermore"">Lorem Ipsum</span>', 'quotes in attr value'); } function testAttributesNeverMentionedAreDropped() { run('<b onclick="evil=true">evil</b>', '<b>evil</b>', 'attrs white-listed'); } function testAttributesNotOverEscaped() { run('<I TITLE="Foo & Bar & Baz">/</I>', '<i title="Foo & Bar & Baz">/</i>', 'attr value not over-escaped'); } function testTagSpecificRulesTakePrecedence() { run('<a title=zogberts>Link</a>', '<a title="<zogberts>">Link</a>', 'tag specific rules take precedence'); } function testAttributeRejectionLocalized() { run('<a id=foo href =//evil.org/ title=>Link</a>', '<a title="<>">Link</a>', 'failure of one attribute does not torpedo others'); } function testWeirdHtmlRulesFollowedForAttrValues() { run('<span title= id=>Lorem Ipsum</span>', '<span title=\"id=\">Lorem Ipsum</span>', 'same as browser on weird values'); } function testAttributesDisallowedOnCloseTags() { run('<h1 title="open">Header</h1 title="closed">', '<h1 title="open">Header</h1>', 'attributes on close tags'); } function testRoundTrippingOfHtmlSafeAgainstIEBacktickProblems() { // Introducing a space at the end of an attribute forces IE to quote it when // turning a DOM into innerHTML which protects against a bunch of problems // with backticks since IE treats them as attribute value delimiters, allowing // foo.innerHTML += ... // to continue to "work" without introducing an XSS vector. // Adding a space at the end is innocuous since HTML attributes whose values // are structured content ignore spaces at the beginning or end. run('<span title="`backtick">*</span>', '<span title="`backtick ">*</span>', 'not round-trippable on IE'); }