// Copyright 2016 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. /** * @fileoverview Unit tests for HTML Sanitizer */ goog.setTestOnly(); goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.html.SafeHtml'); goog.require('goog.html.SafeUrl'); goog.require('goog.html.sanitizer.HtmlSanitizer'); goog.require('goog.html.sanitizer.HtmlSanitizer.Builder'); goog.require('goog.html.sanitizer.TagWhitelist'); goog.require('goog.html.sanitizer.unsafe'); goog.require('goog.html.testing'); goog.require('goog.object'); goog.require('goog.string.Const'); goog.require('goog.testing.dom'); goog.require('goog.testing.jsunit'); goog.require('goog.userAgent'); /** * @return {boolean} Whether the browser is IE8 or below. */ function isIE8() { return goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9); } /** * @return {boolean} Whether the browser is IE9. */ function isIE9() { return goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(10) && !isIE8(); } /** * Sanitizes the original HTML and asserts that it is the same as the expected * HTML. If present the config is passed through to the sanitizer. * @param {string} originalHtml * @param {string} expectedHtml * @param {?goog.html.sanitizer.HtmlSanitizer=} opt_sanitizer */ function assertSanitizedHtml(originalHtml, expectedHtml, opt_sanitizer) { var sanitizer = opt_sanitizer || new goog.html.sanitizer.HtmlSanitizer.Builder().build(); try { var sanitized = sanitizer.sanitize(originalHtml); if (isIE9()) { assertEquals('', goog.html.SafeHtml.unwrap(sanitized)); return; } goog.testing.dom.assertHtmlMatches( expectedHtml, goog.html.SafeHtml.unwrap(sanitized), true /* opt_strictAttributes */); } catch (err) { if (!isIE8()) { throw err; } } if (!opt_sanitizer) { // Retry with raw sanitizer created without the builder. assertSanitizedHtml( originalHtml, expectedHtml, new goog.html.sanitizer.HtmlSanitizer()); // Retry with an explicitly passed in Builder. var builder = new goog.html.sanitizer.HtmlSanitizer.Builder(); assertSanitizedHtml( originalHtml, expectedHtml, new goog.html.sanitizer.HtmlSanitizer(builder)); } } /** * @param {!goog.html.SafeHtml} safeHtml Sanitized HTML which contains a style. * @return {string} cssText contained within SafeHtml. */ function getStyle(safeHtml) { var tmpElement = goog.dom.safeHtmlToNode(safeHtml); return tmpElement.style ? tmpElement.style.cssText : ''; } function testHtmlSanitizeSafeHtml() { var html; html = 'hello world'; assertSanitizedHtml(html, html); html = 'hello world'; assertSanitizedHtml(html, html); html = 'hello world'; assertSanitizedHtml(html, html); html = 'hello world'; assertSanitizedHtml(html, html); // NOTE(user): original did not have tbody html = '
hello world
'; assertSanitizedHtml(html, html); html = '

hello world

'; assertSanitizedHtml(html, html); html = '
hello world
'; assertSanitizedHtml(html, html); html = 'hello world'; assertSanitizedHtml(html, html); html = '
hello world
'; assertSanitizedHtml(html, html); html = '
hello world
'; assertSanitizedHtml(html, html); } // TODO(pelizzi): name of test does not make sense function testDefaultCssSanitizeImage() { var html = '
'; assertSanitizedHtml(html, html); } function testBuilderCanOnlyBeUsedOnce() { var builder = new goog.html.sanitizer.HtmlSanitizer.Builder(); var sanitizer = builder.build(); assertThrows(function() { builder.build(); }); assertThrows(function() { new goog.html.sanitizer.HtmlSanitizer(builder); }); } function testAllowedCssSanitizeImage() { var testUrl = 'http://www.example.com/image3.jpg'; var html = '
'; var sanitizer = new goog.html.sanitizer.HtmlSanitizer.Builder() .allowCssStyles() .withCustomNetworkRequestUrlPolicy(goog.html.SafeUrl.sanitize) .build(); try { var sanitizedHtml = sanitizer.sanitize(html); if (isIE9()) { assertEquals('', goog.html.SafeHtml.unwrap(sanitizedHtml)); return; } assertRegExp( /background(?:-image)?:.*url\(.?http:\/\/www.example.com\/image3.jpg.?\)/, getStyle(sanitizedHtml)); } catch (err) { if (!isIE8()) { throw err; } } } function testHtmlSanitizeXSS() { // NOTE(user): xss cheat sheet found on http://ha.ckers.org/xss.html var safeHtml, xssHtml; // Inserting '; var expected = ''; // TODO(pelizzi): use unblockTag once it's available delete goog.html.sanitizer.TagBlacklist['TEMPLATE']; var builder = new goog.html.sanitizer.HtmlSanitizer.Builder(); goog.html.sanitizer.unsafe.alsoAllowTags(just, builder, ['TEMPLATE']); assertSanitizedHtml(input, expected, builder.build()); goog.html.sanitizer.TagBlacklist['TEMPLATE'] = true; } function testOnlyAllowEmptyAttrList() { var input = '

b

' + 'c'; var expected = '

b

c'; assertSanitizedHtml( input, expected, new goog.html.sanitizer.HtmlSanitizer.Builder() .onlyAllowAttributes([]) .build()); } function testOnlyAllowUnWhitelistedAttr() { assertThrows(function() { new goog.html.sanitizer.HtmlSanitizer.Builder().onlyAllowAttributes( ['alt', 'zzz']); }); } function testOnlyAllowAttributeWildCard() { var input = '
yep
'; var expected = '
yep
'; assertSanitizedHtml( input, expected, new goog.html.sanitizer.HtmlSanitizer.Builder() .onlyAllowAttributes([{tagName: '*', attributeName: 'alt'}]) .build()); } function testOnlyAllowAttributeLabelForA() { var input = 'fff'; var expected = 'fff'; assertSanitizedHtml( input, expected, new goog.html.sanitizer.HtmlSanitizer.Builder() .onlyAllowAttributes([{ tagName: '*', attributeName: 'label', policy: function(value, hints) { if (hints.tagName !== 'a') { return null; } return value; } }]) .build()); } function testOnlyAllowAttributePolicy() { var input = 'yesno'; var expected = 'yes'; assertSanitizedHtml( input, expected, new goog.html.sanitizer.HtmlSanitizer.Builder() .onlyAllowAttributes([{ tagName: '*', attributeName: 'alt', policy: function(value, hints) { assertEquals(hints.attributeName, 'alt'); return value === 'yes' ? value : null; } }]) .build()); } function testOnlyAllowAttributePolicyPipe1() { var input = 'b'; var expected = 'b'; assertSanitizedHtml( input, expected, new goog.html.sanitizer.HtmlSanitizer.Builder() .onlyAllowAttributes([{ tagName: 'a', attributeName: 'target', policy: function(value, hints) { assertEquals(hints.attributeName, 'target'); return '_blank'; } }]) .build()); } function testOnlyAllowAttributePolicyPipe2() { var input = 'b'; var expected = 'b'; assertSanitizedHtml( input, expected, new goog.html.sanitizer.HtmlSanitizer.Builder() .onlyAllowAttributes([{ tagName: 'a', attributeName: 'target', policy: function(value, hints) { assertEquals(hints.attributeName, 'target'); return 'nope'; } }]) .build()); } function testOnlyAllowAttributeSpecificPolicyThrows() { assertThrows(function() { new goog.html.sanitizer.HtmlSanitizer.Builder().onlyAllowAttributes([ {tagName: 'img', attributeName: 'src', policy: goog.functions.identity} ]); }); } function testOnlyAllowAttributeGenericPolicyThrows() { assertThrows(function() { new goog.html.sanitizer.HtmlSanitizer.Builder().onlyAllowAttributes([{ tagName: '*', attributeName: 'target', policy: goog.functions.identity }]); }); } function testOnlyAllowAttributeRefineThrows() { var builder = new goog.html.sanitizer.HtmlSanitizer.Builder() .onlyAllowAttributes( ['aria-checked', {tagName: 'LINK', attributeName: 'HREF'}]) .onlyAllowAttributes(['aria-checked']); assertThrows(function() { builder.onlyAllowAttributes(['alt']); }); } function testUrlWithCredentials() { if (isIE8() || isIE9()) { return; } // IE has trouble getting and setting URL attributes with credentials. Both // HTMLSanitizer and assertHtmlMatches are affected by the bug, hence the use // of plain string matching. var url = 'http://foo:bar@example.com'; var input = '
' + '
'; var expectedIE = '
'; var sanitizer = new goog.html.sanitizer.HtmlSanitizer.Builder() .withCustomNetworkRequestUrlPolicy(goog.html.SafeUrl.sanitize) .allowCssStyles() .build(); if (goog.userAgent.EDGE_OR_IE) { assertEquals( expectedIE, goog.html.SafeHtml.unwrap(sanitizer.sanitize(input))); } else { assertSanitizedHtml(input, input, sanitizer); } }