// 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 = '
';
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 = '';
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 ' +
'a
';
var expected = 'a
' +
'a
';
// 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 =
'';
var expected = '';
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 = '![yes]()
';
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);
}
}