blob: d7846fac9d5229b06d75a747ebc2e5bb7debc643 [file] [log] [blame]
// 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>&lt;&gt;!</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<?...?>>',
'&lt;script&gt;alert(1337)&lt;/script&gt;',
'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&amp;hl=en">Click</a>',
'URL normalized');
}
function testNaiveAttributeRewriterCaught() {
run('<a href="javascript:http://google.com/&#10;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, &quot;Nevermore&quot;">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 &AMP; Bar & Baz">/</I>',
'<i title="Foo &amp; Bar &amp; Baz">/</i>',
'attr value not over-escaped');
}
function testTagSpecificRulesTakePrecedence() {
run('<a title=zogberts>Link</a>',
'<a title="&lt;zogberts&gt;">Link</a>',
'tag specific rules take precedence');
}
function testAttributeRejectionLocalized() {
run('<a id=foo href =//evil.org/ title=>Link</a>',
'<a title="&lt;&gt;">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');
}