accessibility-developer-tools
Version:
This is a library of accessibility-related testing and utility code.
372 lines (313 loc) • 11.8 kB
JavaScript
// 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 testcases for CSS Sanitizer.*/
goog.setTestOnly();
goog.require('goog.array');
goog.require('goog.html.SafeStyle');
goog.require('goog.html.SafeUrl');
goog.require('goog.html.sanitizer.CssSanitizer');
goog.require('goog.string');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
goog.require('goog.userAgent.product');
/**
* @return {boolean} Returns if the browser is IE8.
* @private
*/
function isIE8() {
return goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9);
}
/**
* @return {boolean} Returns if the browser is Safari.
* @private
*/
function isSafari() {
return goog.userAgent.product.SAFARI;
}
/**
* @param {string} cssText CSS text usually associated with an inline style.
* @return {!CSSStyleDeclaration} A styleSheet object.
*/
function getStyleFromCssText(cssText) {
var styleDecleration = document.createElement('div').style;
styleDecleration.cssText = cssText || '';
return styleDecleration;
}
/**
* Asserts that the expected CSS text is equal to the actual CSS text.
* @param {string} expectedCssText Expected CSS text.
* @param {string} actualCssText Actual CSS text.
*/
function assertCSSTextEquals(expectedCssText, actualCssText) {
if (isIE8()) {
// We get a bunch of default values set in IE8 because of the way we iterate
// over the CSSStyleDecleration keys.
// TODO(danesh): Fix IE8 or remove this hack. It will be problematic for
// tests which have an extra semi-colon in the value (even if quoted).
var actualCssArry = actualCssText.split(/\s*;\s*/);
var ie8StyleString = 'WIDTH: 0px; BOTTOM: 0px; HEIGHT: 0px; TOP: 0px; ' +
'RIGHT: 0px; TEXT-DECORATION: none underline overline line-through; ' +
'LEFT: 0px; TEXT-DECORATION: underline line-through;';
goog.array.forEach(ie8StyleString.split(/\s*;\s*/), function(ie8Css) {
goog.array.remove(actualCssArry, ie8Css);
});
actualCssText = actualCssArry.join('; ');
}
assertEquals(
getStyleFromCssText(expectedCssText).cssText,
getStyleFromCssText(actualCssText).cssText);
}
/**
* Get sanitized inline style.
* @param {string} sourceCss CSS to be sanitized.
* @param {function (string, string):?string=} opt_urlRewrite URL rewriter that
* only returns an unwrapped goog.html.SafeUrl.
* @return {string} Sanitized inline style.
* @private
*/
function getSanitizedInlineStyle(sourceCss, opt_urlRewrite) {
try {
return goog.html.SafeStyle.unwrap(
goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle(
getStyleFromCssText(sourceCss), opt_urlRewrite)) ||
'';
} catch (err) {
// IE8 doesn't like setting invalid properties. It throws an "Invalid
// Argument" exception.
if (!isIE8()) {
throw err;
}
return '';
}
}
/**
* Function that mimics sanitization done by the Html sanitizer.
* @param {string} url Original url
* @return {?string} Sanitized url
* @private
*/
function originalUrl(url) {
var sanitizedUrl = goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(url));
if (sanitizedUrl == goog.html.SafeUrl.INNOCUOUS_STRING) {
return null;
}
return sanitizedUrl;
}
function testValidCss() {
var actualCSS = 'font-family: inherit';
var expectedCSS = 'font-family: inherit';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
// .1 -> 0.1; 1.0 -> 1
actualCSS = 'padding: 1pt .1pt 1pt 1.0em';
expectedCSS = 'padding: 1pt 0.1pt 1pt 1em';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
// Negative margins are allowed.
actualCSS = 'margin: -7px -.5px -23px -1.25px';
expectedCSS = 'margin: -7px -0.5px -23px -1.25px';
if (isIE8()) {
// IE8 doesn't like sub-pixels
// https://blogs.msdn.microsoft.com/ie/2010/11/03/sub-pixel-fonts-in-ie9/
expectedCSS = expectedCSS.replace('-1.25px', '-1px');
}
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
actualCSS = 'quotes: "{" "}" "<" ">"';
expectedCSS = 'quotes: "{" "}" "<" ">";';
if (isSafari()) {
// TODO(danesh): Figure out what is wrong with WebKit (Safari).
expectedCSS = 'quotes: \'{\';';
}
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
}
function testInvalidCssRemoved() {
var actualCSS;
// Tests all have null results.
var expectedCSS = '';
actualCSS = 'font: Arial Black,monospace,Helvetica,#88ff88';
// Hash values are not allowed so are dropped.
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
// Negative numbers for border not allowed.
actualCSS = 'border : -7px -0.5px -23px -1.25px';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
// Negative numbers converted to empty.
actualCSS = 'padding: -0 -.0 -0. -0.0 ';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
// Invalid values not allowed.
actualCSS = 'padding : #123 - 5 "5"';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
// Font-family does not allow quantities at all.
actualCSS = 'font-family: 7 .5 23 1.25 -7 -.5 -23 -1.25 +7 +.5 +23 +1.25 ' +
'7cm .5em 23.mm 1.25px -7cm -.5em -23.mm -1.25px ' +
'+7cm +.5em +23.mm +1.25px 0 .0 -0+00.0 /';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
actualCSS = 'background: bogus url("foo.png") transparent';
assertCSSTextEquals(
expectedCSS, getSanitizedInlineStyle(actualCSS, originalUrl));
// expression(...) is not allowed for font so is rejected wholesale -- the
// internal string "pwned" is not passed through.
actualCSS = 'font-family: Arial Black,monospace,expression(return "pwned"),' +
'Helvetica,#88ff88';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
}
function testCssBackground() {
var actualCSS, expectedCSS;
function proxyUrl(url) { return 'https://goo.gl/proxy?url=' + url; }
// Don't require the URL sanitizer to protect string boundaries.
actualCSS = 'background-image: url("javascript:evil(1337)")';
expectedCSS = '';
assertCSSTextEquals(
expectedCSS, getSanitizedInlineStyle(actualCSS, originalUrl));
actualCSS = 'background-image: url("http://goo.gl/foo.png")';
expectedCSS =
'background-image: url(https://goo.gl/proxy?url=http://goo.gl/foo.png)';
assertCSSTextEquals(
expectedCSS, getSanitizedInlineStyle(actualCSS, proxyUrl));
// Without any URL sanitizer.
actualCSS = 'background: transparent url("Bar.png")';
var sanitizedCss = getSanitizedInlineStyle(actualCSS);
assertFalse(goog.string.contains(sanitizedCss, 'background-image'));
assertFalse(goog.string.contains(sanitizedCss, 'Bar.png'));
}
function testVendorPrefixed() {
var actualCSS = '-webkit-text-stroke: calc(3px - 2px) red';
var expectedCSS = '';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
}
function testColor() {
var colors = [
'red', 'Red', 'RED', 'Gray', 'grey', '#abc', '#123', '#ABC123',
'rgb( 127, 64 , 255 )'
];
var notcolors = [
// Finding words that are not X11 colors is harder than you think.
'killitwithfire', 'invisible', 'expression(red=blue)', '#aa-1bb',
'#expression', '#doevil'
// 'rgb(0, 0, 100%)' // Invalid in all browsers
// 'rgba(128,255,128,50%)', // Invalid in all browsers
];
for (var i = 0; i < colors.length; ++i) {
var validColorValue = 'color: ' + colors[i];
assertCSSTextEquals(
// Firefox doesn't "normalize" the CSS text unless a property is
// modified, so need to lower case it prior to comparison.
validColorValue.toLowerCase(),
getSanitizedInlineStyle(validColorValue));
}
for (var i = 0; i < notcolors.length; ++i) {
var invalidColorValue = 'color: ' + notcolors[i];
assertCSSTextEquals('', getSanitizedInlineStyle(invalidColorValue));
}
}
function testCustomVariablesSanitized() {
var actualCSS = '\\2d-leak: leakTest; background: var(--leak);';
assertCSSTextEquals('', getSanitizedInlineStyle(actualCSS));
}
function testExpressionsPreserved() {
if (isIE8()) {
// Disable this test as IE8 doesn't support expressions.
// https://msdn.microsoft.com/en-us/library/ms537634(v=VS.85).aspx
return;
}
var actualCSS, expectedCSS;
actualCSS = 'background-image: linear-gradient(to bottom right, red, blue)';
expectedCSS = 'background-image: linear-gradient(to right bottom, red, blue)';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
}
function testMultipleInlineStyles() {
var actualCSS = 'margin: 1px ; padding: 0';
var expectedCSS = 'margin: 1px; padding: 0px;';
assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
}
function testSanitizeInlineStyleString() {
var tests = [
{
// empty string
inputCss: '',
sanitizedCss: ''
},
{
// one rule
inputCss: 'color: red',
sanitizedCss: 'color: red;'
},
{
// two rules
inputCss: 'color: green; padding: 10px',
sanitizedCss: 'color: green; padding: 10px;'
},
{
// malicious rule
inputCss: 'color: expression("pwned")',
sanitizedCss: ''
},
{
// disallowed URL
inputCss: 'background-image: url("http://example.com")',
sanitizedCss: ''
},
{
// disallowed URL
inputCss: 'background-image: url("http://example.com")',
sanitizedCss: '',
uriRewriter: function(uri) { return null; }
},
{
// allowed URL
inputCss: 'background-image: url("http://example.com")',
sanitizedCss: 'background-image: url("http://example.com");',
uriRewriter: function(uri) { return uri; }
}
];
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
var expectedOutput = test.sanitizedCss;
if (goog.userAgent.IE && document.documentMode < 10) {
expectedOutput = '';
}
var safeStyle = goog.html.sanitizer.CssSanitizer.sanitizeInlineStyleString(
test.inputCss, test.uriRewriter);
var output = goog.html.SafeStyle.unwrap(safeStyle);
assertCSSTextEquals(expectedOutput, output);
}
}
/**
* @suppress {accessControls}
*/
function testInertDocument() {
if (!document.implementation.createHTMLDocument) {
return; // skip test
}
window.xssFiredInertDocument = false;
var doc = goog.html.sanitizer.CssSanitizer.createInertDocument_();
try {
doc.write('<script> window.xssFiredInertDocument = true; </script>');
} catch (e) {
// ignore
}
assertFalse(window.xssFiredInertDocument);
}
/**
* @suppress {accessControls}
*/
function testInertCustomElements() {
if (typeof HTMLTemplateElement != 'function' || !document.registerElement) {
return; // skip test
}
var inertDoc = goog.html.sanitizer.CssSanitizer.createInertDocument_();
var xFooConstructor = document.registerElement('x-foo');
var xFooElem =
document.implementation.createHTMLDocument('').createElement('x-foo');
assertTrue(xFooElem instanceof xFooConstructor); // sanity check
var inertXFooElem = inertDoc.createElement('x-foo');
assertFalse(inertXFooElem instanceof xFooConstructor);
}