csssanitizer_test.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. // Copyright 2016 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /** @fileoverview testcases for CSS Sanitizer.*/
  15. goog.setTestOnly();
  16. goog.require('goog.array');
  17. goog.require('goog.html.SafeStyle');
  18. goog.require('goog.html.SafeUrl');
  19. goog.require('goog.html.sanitizer.CssSanitizer');
  20. goog.require('goog.html.testing');
  21. goog.require('goog.string');
  22. goog.require('goog.testing.jsunit');
  23. goog.require('goog.userAgent');
  24. goog.require('goog.userAgent.product');
  25. /**
  26. * @return {boolean} Returns if the browser is IE8.
  27. * @private
  28. */
  29. function isIE8() {
  30. return goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9);
  31. }
  32. /**
  33. * @return {boolean} Returns if the browser is Safari.
  34. * @private
  35. */
  36. function isSafari() {
  37. return goog.userAgent.product.SAFARI;
  38. }
  39. /**
  40. * @param {string} cssText CSS text usually associated with an inline style.
  41. * @return {!CSSStyleDeclaration} A styleSheet object.
  42. */
  43. function getStyleFromCssText(cssText) {
  44. var styleDecleration = document.createElement('div').style;
  45. styleDecleration.cssText = cssText || '';
  46. return styleDecleration;
  47. }
  48. /**
  49. * Asserts that the expected CSS text is equal to the actual CSS text.
  50. * @param {string} expectedCssText Expected CSS text.
  51. * @param {string} actualCssText Actual CSS text.
  52. */
  53. function assertCSSTextEquals(expectedCssText, actualCssText) {
  54. if (isIE8()) {
  55. // We get a bunch of default values set in IE8 because of the way we iterate
  56. // over the CSSStyleDecleration keys.
  57. // TODO(danesh): Fix IE8 or remove this hack. It will be problematic for
  58. // tests which have an extra semi-colon in the value (even if quoted).
  59. var actualCssArry = actualCssText.split(/\s*;\s*/);
  60. var ie8StyleString = 'WIDTH: 0px; BOTTOM: 0px; HEIGHT: 0px; TOP: 0px; ' +
  61. 'RIGHT: 0px; TEXT-DECORATION: none underline overline line-through; ' +
  62. 'LEFT: 0px; TEXT-DECORATION: underline line-through;';
  63. goog.array.forEach(ie8StyleString.split(/\s*;\s*/), function(ie8Css) {
  64. goog.array.remove(actualCssArry, ie8Css);
  65. });
  66. actualCssText = actualCssArry.join('; ');
  67. }
  68. assertEquals(
  69. getStyleFromCssText(expectedCssText).cssText,
  70. getStyleFromCssText(actualCssText).cssText);
  71. }
  72. /**
  73. * Gets sanitized inline style.
  74. * @param {string} sourceCss CSS to be sanitized.
  75. * @param {function (string, string):?goog.html.SafeUrl=} opt_urlRewrite URL
  76. * rewriter that only returns a goog.html.SafeUrl.
  77. * @return {string} Sanitized inline style.
  78. * @private
  79. */
  80. function getSanitizedInlineStyle(sourceCss, opt_urlRewrite) {
  81. try {
  82. return goog.html.SafeStyle.unwrap(
  83. goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle(
  84. getStyleFromCssText(sourceCss), opt_urlRewrite)) ||
  85. '';
  86. } catch (err) {
  87. // IE8 doesn't like setting invalid properties. It throws an "Invalid
  88. // Argument" exception.
  89. if (!isIE8()) {
  90. throw err;
  91. }
  92. return '';
  93. }
  94. }
  95. function testValidCss() {
  96. var actualCSS = 'font-family: inherit';
  97. var expectedCSS = 'font-family: inherit';
  98. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  99. // .1 -> 0.1; 1.0 -> 1
  100. actualCSS = 'padding: 1pt .1pt 1pt 1.0em';
  101. expectedCSS = 'padding: 1pt 0.1pt 1pt 1em';
  102. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  103. // Negative margins are allowed.
  104. actualCSS = 'margin: -7px -.5px -23px -1.25px';
  105. expectedCSS = 'margin: -7px -0.5px -23px -1.25px';
  106. if (isIE8()) {
  107. // IE8 doesn't like sub-pixels
  108. // https://blogs.msdn.microsoft.com/ie/2010/11/03/sub-pixel-fonts-in-ie9/
  109. expectedCSS = expectedCSS.replace('-0.5px', '0px');
  110. expectedCSS = expectedCSS.replace('-1.25px', '-1px');
  111. }
  112. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  113. actualCSS = 'quotes: "{" "}" "<" ">"';
  114. expectedCSS = 'quotes: "{" "}" "<" ">";';
  115. if (isSafari()) {
  116. // TODO(danesh): Figure out what is wrong with WebKit (Safari).
  117. expectedCSS = 'quotes: \'{\';';
  118. }
  119. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  120. }
  121. function testInvalidCssRemoved() {
  122. var actualCSS;
  123. // Tests all have null results.
  124. var expectedCSS = '';
  125. actualCSS = 'font: Arial Black,monospace,Helvetica,#88ff88';
  126. // Hash values are not allowed so are dropped.
  127. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  128. // Negative numbers for border not allowed.
  129. actualCSS = 'border : -7px -0.5px -23px -1.25px';
  130. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  131. // Negative numbers converted to empty.
  132. actualCSS = 'padding: -0 -.0 -0. -0.0 ';
  133. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  134. // Invalid values not allowed.
  135. actualCSS = 'padding : #123 - 5 "5"';
  136. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  137. // Font-family does not allow quantities at all.
  138. actualCSS = 'font-family: 7 .5 23 1.25 -7 -.5 -23 -1.25 +7 +.5 +23 +1.25 ' +
  139. '7cm .5em 23.mm 1.25px -7cm -.5em -23.mm -1.25px ' +
  140. '+7cm +.5em +23.mm +1.25px 0 .0 -0+00.0 /';
  141. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  142. actualCSS = 'background: bogus url("foo.png") transparent';
  143. assertCSSTextEquals(
  144. expectedCSS,
  145. getSanitizedInlineStyle(actualCSS, goog.html.SafeUrl.sanitize));
  146. // expression(...) is not allowed for font so is rejected wholesale -- the
  147. // internal string "pwned" is not passed through.
  148. actualCSS = 'font-family: Arial Black,monospace,expression(return "pwned"),' +
  149. 'Helvetica,#88ff88';
  150. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  151. }
  152. function testCssBackground() {
  153. var actualCSS, expectedCSS;
  154. function proxyUrl(url) {
  155. return goog.html.testing.newSafeUrlForTest(
  156. 'https://goo.gl/proxy?url=' + url);
  157. }
  158. // Don't require the URL sanitizer to protect string boundaries.
  159. actualCSS = 'background-image: url("javascript:evil(1337)")';
  160. expectedCSS = '';
  161. assertCSSTextEquals(
  162. expectedCSS,
  163. getSanitizedInlineStyle(actualCSS, goog.html.SafeUrl.sanitize));
  164. actualCSS = 'background-image: url("http://goo.gl/foo.png")';
  165. expectedCSS =
  166. 'background-image: url(https://goo.gl/proxy?url=http://goo.gl/foo.png)';
  167. assertCSSTextEquals(
  168. expectedCSS, getSanitizedInlineStyle(actualCSS, proxyUrl));
  169. // Without any URL sanitizer.
  170. actualCSS = 'background: transparent url("Bar.png")';
  171. var sanitizedCss = getSanitizedInlineStyle(actualCSS);
  172. assertFalse(goog.string.contains(sanitizedCss, 'background-image'));
  173. assertFalse(goog.string.contains(sanitizedCss, 'Bar.png'));
  174. }
  175. function testVendorPrefixed() {
  176. var actualCSS = '-webkit-text-stroke: 1px red';
  177. var expectedCSS = '';
  178. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  179. }
  180. function testDisallowedFunction() {
  181. var actualCSS = 'border-width: calc(10px + 20px)';
  182. var expectedCSS = '';
  183. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  184. }
  185. function testColor() {
  186. var colors = [
  187. 'red', 'Red', 'RED', 'Gray', 'grey', '#abc', '#123', '#ABC123',
  188. 'rgb( 127, 64 , 255 )'
  189. ];
  190. var notcolors = [
  191. // Finding words that are not X11 colors is harder than you think.
  192. 'killitwithfire', 'invisible', 'expression(red=blue)', '#aa-1bb',
  193. '#expression', '#doevil'
  194. // 'rgb(0, 0, 100%)' // Invalid in all browsers
  195. // 'rgba(128,255,128,50%)', // Invalid in all browsers
  196. ];
  197. for (var i = 0; i < colors.length; ++i) {
  198. var validColorValue = 'color: ' + colors[i];
  199. assertCSSTextEquals(
  200. validColorValue, getSanitizedInlineStyle(validColorValue));
  201. }
  202. for (var i = 0; i < notcolors.length; ++i) {
  203. var invalidColorValue = 'color: ' + notcolors[i];
  204. assertCSSTextEquals('', getSanitizedInlineStyle(invalidColorValue));
  205. }
  206. }
  207. function testCustomVariablesSanitized() {
  208. var actualCSS = '\\2d-leak: leakTest; background: var(--leak);';
  209. assertCSSTextEquals('', getSanitizedInlineStyle(actualCSS));
  210. }
  211. function testExpressionsPreserved() {
  212. if (isIE8()) {
  213. // Disable this test as IE8 doesn't support expressions.
  214. // https://msdn.microsoft.com/en-us/library/ms537634(v=VS.85).aspx
  215. return;
  216. }
  217. var actualCSS, expectedCSS;
  218. actualCSS = 'background-image: linear-gradient(to bottom right, red, blue)';
  219. expectedCSS = 'background-image: linear-gradient(to right bottom, red, blue)';
  220. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  221. }
  222. function testMultipleInlineStyles() {
  223. var actualCSS = 'margin: 1px ; padding: 0';
  224. var expectedCSS = 'margin: 1px; padding: 0px;';
  225. assertCSSTextEquals(expectedCSS, getSanitizedInlineStyle(actualCSS));
  226. }
  227. function testSanitizeInlineStyleString() {
  228. var tests = [
  229. {
  230. // empty string
  231. inputCss: '',
  232. sanitizedCss: ''
  233. },
  234. {
  235. // one rule
  236. inputCss: 'color: red',
  237. sanitizedCss: 'color: red;'
  238. },
  239. {
  240. // two rules
  241. inputCss: 'color: green; padding: 10px',
  242. sanitizedCss: 'color: green; padding: 10px;'
  243. },
  244. {
  245. // malicious rule
  246. inputCss: 'color: expression("pwned")',
  247. sanitizedCss: ''
  248. },
  249. {
  250. // disallowed URL
  251. inputCss: 'background-image: url("http://example.com")',
  252. sanitizedCss: ''
  253. },
  254. {
  255. // disallowed URL
  256. inputCss: 'background-image: url("http://example.com")',
  257. sanitizedCss: '',
  258. uriRewriter: function(uri) {
  259. return null;
  260. }
  261. },
  262. {
  263. // allowed URL
  264. inputCss: 'background-image: url("http://example.com")',
  265. sanitizedCss: 'background-image: url("http://example.com");',
  266. uriRewriter: goog.html.SafeUrl.sanitize
  267. },
  268. {
  269. // preserves case
  270. inputCss: 'font-family: Roboto, sans-serif',
  271. sanitizedCss: 'font-family: Roboto, sans-serif'
  272. }
  273. ];
  274. for (var i = 0; i < tests.length; i++) {
  275. var test = tests[i];
  276. var expectedOutput = test.sanitizedCss;
  277. if (goog.userAgent.IE && document.documentMode < 10) {
  278. expectedOutput = '';
  279. }
  280. var safeStyle = goog.html.sanitizer.CssSanitizer.sanitizeInlineStyleString(
  281. test.inputCss, test.uriRewriter);
  282. var output = goog.html.SafeStyle.unwrap(safeStyle);
  283. assertCSSTextEquals(expectedOutput, output);
  284. }
  285. }
  286. /**
  287. * @suppress {accessControls}
  288. */
  289. function testInertDocument() {
  290. if (!document.implementation.createHTMLDocument) {
  291. return; // skip test
  292. }
  293. window.xssFiredInertDocument = false;
  294. var doc = goog.html.sanitizer.CssSanitizer.createInertDocument_();
  295. try {
  296. doc.write('<script> window.xssFiredInertDocument = true; </script>');
  297. } catch (e) {
  298. // ignore
  299. }
  300. assertFalse(window.xssFiredInertDocument);
  301. }
  302. /**
  303. * @suppress {accessControls}
  304. */
  305. function testInertCustomElements() {
  306. if (typeof HTMLTemplateElement != 'function' || !document.registerElement) {
  307. return; // skip test
  308. }
  309. var inertDoc = goog.html.sanitizer.CssSanitizer.createInertDocument_();
  310. var xFooConstructor = document.registerElement('x-foo');
  311. var xFooElem =
  312. document.implementation.createHTMLDocument('').createElement('x-foo');
  313. assertTrue(xFooElem instanceof xFooConstructor); // sanity check
  314. var inertXFooElem = inertDoc.createElement('x-foo');
  315. assertFalse(inertXFooElem instanceof xFooConstructor);
  316. }