UNPKG

@apache-royale/royale-js-swf

Version:

Apache Royale (formerly FlexJS)

710 lines (591 loc) 23.2 kB
// 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.provide('goog.html.CssSanitizerTest'); goog.setTestOnly(); goog.require('goog.array'); goog.require('goog.html.SafeStyle'); goog.require('goog.html.SafeStyleSheet'); goog.require('goog.html.SafeUrl'); goog.require('goog.html.sanitizer.CssSanitizer'); goog.require('goog.html.testing'); goog.require('goog.string'); goog.require('goog.testing.dom'); goog.require('goog.testing.jsunit'); goog.require('goog.userAgent'); goog.require('goog.userAgent.product'); goog.require('goog.userAgent.product.isVersion'); var isIE8 = goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9); var isSafari9OrOlder = goog.userAgent.product.SAFARI && !goog.userAgent.product.isVersion(10); var supportsDomParser = !goog.userAgent.IE || goog.userAgent.isVersionOrHigher(10); /** * @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 actualCssArray = 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(actualCssArray, ie8Css); }); actualCssText = actualCssArray.join('; '); } assertEquals( getStyleFromCssText(expectedCssText).cssText, getStyleFromCssText(actualCssText).cssText); } /** * Gets sanitized inline style. * @param {string} sourceCss CSS to be sanitized. * @param {function (string, string):?goog.html.SafeUrl=} opt_urlRewrite URL * rewriter that only returns a goog.html.SafeUrl. * @return {string} Sanitized inline style. */ 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 ''; } } /** * Asserts that the input CSS text is equal to the expected CSS text after * sanitization using {@link CssSanitizer.sanitizeStyleSheetString}. * @param {string} expectedCssText * @param {string} inputCssText * @param {?string=} opt_containerId * @param {function(string, string):?SafeUrl=} opt_uriRewriter */ function assertSanitizedCssEquals( expectedCssText, inputCssText, opt_containerId, opt_uriRewriter) { assertBrowserSanitizedCssEquals( {chrome: expectedCssText}, inputCssText, opt_containerId, opt_uriRewriter); } /** * Asserts that on each browser the input CSS text is equal to the expected CSS * text after sanitization using {@link CssSanitizer.sanitizeStyleSheetString}. * Automatically verifies that on older browsers the sanitizer returns an empty * string. * @param {{ * chrome: string, * firefox: (string|undefined), * safari: (string|undefined), * newIE: (string|undefined), * ie10: (string|undefined)}} expectedCssTextByBrowser An object that maps * each browser to a different expected value. All browsers but chrome are * optional. If a browser is missing, the chrome expected value is used * instead. * @param {string} inputCssText * @param {?string=} opt_containerId * @param {function(string, string):?SafeUrl=} opt_uriRewriter */ function assertBrowserSanitizedCssEquals( expectedCssTextByBrowser, inputCssText, opt_containerId, opt_uriRewriter) { var expectedCssText = undefined; if (goog.userAgent.product.CHROME) { expectedCssText = expectedCssTextByBrowser.chrome; } else if (goog.userAgent.product.FIREFOX) { expectedCssText = expectedCssTextByBrowser.firefox; } else if (goog.userAgent.product.SAFARI) { expectedCssText = expectedCssTextByBrowser.safari; } else if (goog.userAgent.product.IE && document.documentMode == 10) { expectedCssText = expectedCssTextByBrowser.ie10; console.log('ie10'); } else if ( goog.userAgent.product.IE && !goog.userAgent.isVersionOrHigher(10)) { // Don't even try with chrome as default for IE8 and IE9. expectedCssText = ''; } else if (goog.userAgent.product.IE || goog.userAgent.product.EDGE) { expectedCssText = expectedCssTextByBrowser.newIE; } else { throw new Error('Unrecognized browser, this function needs to be updated.'); } if (expectedCssText == undefined) { expectedCssText = expectedCssTextByBrowser.chrome; } assertEquals( expectedCssText, goog.html.SafeStyleSheet.unwrap( goog.html.sanitizer.CssSanitizer.sanitizeStyleSheetString( inputCssText, opt_containerId === undefined ? 'foo' : opt_containerId, opt_uriRewriter))); } /** * Converts rules in STYLE tags into style attributes on the tags they apply to, * and returns a new string. * @param {string} html * @return {string} */ function inlineStyleRulesString(html) { var tree = goog.html.sanitizer.CssSanitizer.safeParseHtmlAndGetInertElement( '<span>' + html + '</span>'); if (tree == null) { return ''; } goog.html.sanitizer.CssSanitizer.inlineStyleRules(tree); return tree.innerHTML; } /** * Inlines the rules in STYLE tags in the original HTML and asserts that it is * the same as the expected HTML. * @param {string} expectedHtml * @param {string} originalHtml */ function assertInlinedStyles(expectedHtml, originalHtml) { var inlinedHtml = inlineStyleRulesString(originalHtml); if (!supportsDomParser) { assertEquals('', inlinedHtml); return; } goog.testing.dom.assertHtmlMatches( expectedHtml, inlinedHtml, true /* opt_strictAttributes */); } 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('-0.5px', '0px'); expectedCSS = expectedCSS.replace('-1.25px', '-1px'); } assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS)); actualCSS = 'quotes: "{" "}" "<" ">"'; expectedCSS = 'quotes: "{" "}" "<" ">";'; if (isSafari9OrOlder) { // We never figured out why Safari didn't work here, but it's obsolete now. 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, goog.html.SafeUrl.sanitize)); // 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 goog.html.testing.newSafeUrlForTest( '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, goog.html.SafeUrl.sanitize)); 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: 1px red'; var expectedCSS = ''; assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS)); } function testDisallowedFunction() { var actualCSS = 'border-width: calc(10px + 20px)'; 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( validColorValue, 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: goog.html.SafeUrl.sanitize }, { // preserves case inputCss: 'font-family: Roboto, sans-serif', sanitizedCss: 'font-family: Roboto, sans-serif' } ]; 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); } function testSanitizeStyleSheetString_basic() { var input = ''; assertSanitizedCssEquals(input, input); input = 'a {color: red}'; var expected = '#foo a{color: red;}'; assertSanitizedCssEquals(expected, input); input = 'a {color: red} b {color:red; not-allowed: 1; ' + 'background-image: url(\'http://not.allowed\');}'; expected = '#foo a{color: red;}#foo b{color: red;}'; assertSanitizedCssEquals(expected, input); } function testSanitizeInlineStyleString_noSelector() { var input = 'a{color: red;}'; assertSanitizedCssEquals(input, input, null /* opt_containerId */); } function testSanitizeStyleSheetString_comma() { var input = 'a, b, c > d {color: red}'; var expected = '#foo a,#foo b,#foo c > d{color: red;}'; assertSanitizedCssEquals(expected, input); } function testSanitizeStyleSheetString_atRule() { var input = '@media screen { a { color: red; } }'; var expected = ''; assertSanitizedCssEquals(expected, input); } function testSanitizeStyleSheetString_urlRewrite() { var urlRewriter = function(url) { if (input.indexOf('bar') > -1) { return goog.html.SafeUrl.sanitize(url); } else { return null; } }; var input = 'a {background-image: url("http://bar.com")}'; var quoted = '#foo a{background-image: url("http://bar.com");}'; // Safari will strip quotes if they are not needed and add a slash. var unquoted = '#foo a{background-image: url(http://bar.com/);}'; assertBrowserSanitizedCssEquals( {safari: unquoted, chrome: quoted}, input, undefined /* opt_containerId */, urlRewriter); input = 'a {background-image: url("http://nope.com")}'; var expected = '#foo a{}'; assertSanitizedCssEquals( expected, input, undefined /* opt_containerId */, urlRewriter); input = 'a{background-image: url("javascript:alert(\"bar\")")}'; expected = '#foo a{}'; assertSanitizedCssEquals( expected, input, undefined /* opt_containerId */, urlRewriter); } function testSanitizeStyleSheetString_unrecognized() { var input = 'a {mso-font-size: 2px; color: black}'; var expected = 'a{color: black;}'; assertSanitizedCssEquals(expected, input, null /* opt_containerId */); } function testSanitizeStyleSheetString_malformed() { var input = '<script>alert(1)</script>'; var expected = ''; assertSanitizedCssEquals(expected, input); input = 'a { } </style><script>alert(1)</script><style>'; expected = '#foo a{}'; assertSanitizedCssEquals(expected, input); input = 'a < b { } a { font-size: 10px }'; expected = '#foo a{font-size: 10px;}'; assertSanitizedCssEquals(expected, input); input = 'a {;;;} a { font-size: 10px } ;;; a { background-image: url(() }};;'; expected = '#foo a{}#foo a{font-size: 10px;}'; assertSanitizedCssEquals(expected, input); input = 'a[a=\"ccc] { color: red;}'; expected = ''; assertSanitizedCssEquals(expected, input); } function testSanitizeStyleSheetString_stringWithNoEscapedQuotesInSelector() { var input = 'a[data-foo="foo,bar"], b { color: red }'; // IE converts the string to single quotes. var doubleQuoted = '#foo a[data-foo="foo,bar"],#foo b{color: red;}'; var singleQuoted = '#foo a[data-foo=\'foo,bar\'],#foo b{color: red;}'; assertBrowserSanitizedCssEquals( {chrome: doubleQuoted, newIE: singleQuoted, ie10: singleQuoted}, input); input = 'a[data-foo=\'foo,bar\'], b { color: red }'; // Chrome converts the string to double quotes. doubleQuoted = '#foo a[data-foo="foo,bar"],#foo b{color: red;}'; singleQuoted = '#foo a[data-foo=\'foo,bar\'],#foo b{color: red;}'; assertBrowserSanitizedCssEquals( {chrome: doubleQuoted, newIE: singleQuoted, ie10: singleQuoted}, input); input = 'a[foo="foo,bar"][bar="baz"], b { color: blue }'; doubleQuoted = '#foo a[foo="foo,bar"][bar="baz"],#foo b{color: blue;}'; singleQuoted = '#foo a[foo=\'foo,bar\'][bar=\'baz\'],#foo b{color: blue;}'; assertBrowserSanitizedCssEquals( {chrome: doubleQuoted, newIE: singleQuoted, ie10: singleQuoted}, input); input = 'a[foo="foo,bar"], b, c[foo="f,b"][bar="f,b"] { color: red }'; doubleQuoted = '#foo a[foo="foo,bar"],#foo b,#foo c[foo="f,b"][bar="f,b"]' + '{color: red;}'; singleQuoted = '#foo a[foo=\'foo,bar\'],#foo b,#foo c[bar=\'f,b\'][foo=\'f,b\']' + '{color: red;}'; assertBrowserSanitizedCssEquals( {chrome: doubleQuoted, newIE: singleQuoted, ie10: singleQuoted}, input); } function testSanitizeStyleSheetString_stringWithEscapedQuotesInSelector() { // Contains an escaped string, but the selector is converted to a[a='a"b'] // before the regex is executed. var input = 'a[a="a\\"b"] { color: black; }'; var doubleQuoted = '#foo a[a="a\\"b"]{color: black;}'; var singleQuoted = '#foo a[a=\'a"b\']{color: black;}'; assertBrowserSanitizedCssEquals( {chrome: doubleQuoted, ie10: singleQuoted, newIE: singleQuoted}, input); input = 'a[a="a\\\'b"] { color: grey; }'; doubleQuoted = '#foo a[a="a\'b"]{color: grey;}'; singleQuoted = '#foo a[a=\'a\\\'b\']{color: grey;}'; assertBrowserSanitizedCssEquals( { chrome: doubleQuoted, firefox: doubleQuoted, ie10: '', newIE: singleQuoted }, input); input = 'a[a=\'\\\'b\'] {color: red; }'; doubleQuoted = '#foo a[a="\'b"]{color: red;}'; singleQuoted = '#foo a[a=\'\\\'b\']{color: red;}'; assertBrowserSanitizedCssEquals( { chrome: doubleQuoted, firefox: doubleQuoted, newIE: singleQuoted, ie10: '' }, input); input = 'a[foo=\'b\\\'a, ,\'] { color: blue; }'; doubleQuoted = '#foo a[foo="b\'a, ,"]{color: blue;}'; singleQuoted = '#foo a[foo=\'b\\\'a, ,\']{color: blue;}'; assertBrowserSanitizedCssEquals( { chrome: doubleQuoted, firefox: doubleQuoted, newIE: singleQuoted, ie10: '' }, input); input = 'a[a=\'a\\"b\'] { color: black; }'; doubleQuoted = '#foo a[a="a\\"b"]{color: black;}'; singleQuoted = '#foo a[a=\'a"b\']{color: black;}'; assertBrowserSanitizedCssEquals( {chrome: doubleQuoted, ie10: singleQuoted, newIE: singleQuoted}, input); } function testSanitizeInlineStyleString_invalidSelector() { var input = 'a{}'; if (supportsDomParser) { assertThrows(function() { goog.html.sanitizer.CssSanitizer.sanitizeStyleSheetString( input, '</style>'); }); } } function testInlineStyleRules_empty() { assertInlinedStyles('', ''); } function testInlineStyleRules_basic() { var input = '<style>a{color:red}</style><a>foo</a>'; var expected = '<a style="color:red;">foo</a>'; assertInlinedStyles(expected, input); } function testInlineStyleRules_onlyStyle() { var input = '<style>a{color:red}</style>'; assertInlinedStyles('', input); } function testInlineStyleRules_noStyle() { var input = '<a>hi</a>'; assertInlinedStyles(input, input); } function testInlineStyleRules_onlyText() { var input = 'hello'; assertInlinedStyles(input, input); } function testInlineStyleRules_specificity() { var input = '<style>a{color: red; border: 1px}' + '#foo{color: white; border: 2px}</style>' + '<a id="foo" style="color: black">foo</a>'; var expected = '<a id="foo" style="color: black; border: 2px">foo</a>'; assertInlinedStyles(expected, input); } function testInlineStyleRules_media() { var input = '<style>a{color: red;} @media screen { border: 1px; }</style>' + '<a id="foo">foo</a>'; var expected = '<a id="foo" style="color: red;">foo</a>'; assertInlinedStyles(expected, input); }