csssanitizer.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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. /**
  15. * @fileoverview
  16. * JavaScript support for client-side CSS sanitization.
  17. *
  18. * @author danesh@google.com (Danesh Irani)
  19. * @author mikesamuel@gmail.com (Mike Samuel)
  20. */
  21. goog.provide('goog.html.sanitizer.CssSanitizer');
  22. goog.require('goog.array');
  23. goog.require('goog.dom');
  24. goog.require('goog.dom.TagName');
  25. goog.require('goog.html.SafeStyle');
  26. goog.require('goog.html.SafeUrl');
  27. goog.require('goog.html.uncheckedconversions');
  28. goog.require('goog.object');
  29. goog.require('goog.string');
  30. /**
  31. * The set of characters that need to be normalized inside url("...").
  32. * We normalize newlines because they are not allowed inside quoted strings,
  33. * normalize quote characters, angle-brackets, and asterisks because they
  34. * could be used to break out of the URL or introduce targets for CSS
  35. * error recovery. We normalize parentheses since they delimit unquoted
  36. * URLs and calls and could be a target for error recovery.
  37. * @const @private {!RegExp}
  38. */
  39. goog.html.sanitizer.CssSanitizer.NORM_URL_REGEXP_ = /[\n\f\r\"\'()*<>]/g;
  40. /**
  41. * The replacements for NORM_URL_REGEXP.
  42. * @private @const {!Object<string, string>}
  43. */
  44. goog.html.sanitizer.CssSanitizer.NORM_URL_REPLACEMENTS_ = {
  45. '\n': '%0a',
  46. '\f': '%0c',
  47. '\r': '%0d',
  48. '"': '%22',
  49. '\'': '%27',
  50. '(': '%28',
  51. ')': '%29',
  52. '*': '%2a',
  53. '<': '%3c',
  54. '>': '%3e'
  55. };
  56. /**
  57. * Normalizes a character for use in a url() directive.
  58. * @param {string} ch Character to be normalized.
  59. * @return {?string} Normalized character.
  60. * @private
  61. */
  62. goog.html.sanitizer.CssSanitizer.normalizeUrlChar_ = function(ch) {
  63. return goog.html.sanitizer.CssSanitizer.NORM_URL_REPLACEMENTS_[ch] || null;
  64. };
  65. /**
  66. * Constructs a safe URI from a given URI and prop using a given uriRewriter
  67. * function.
  68. * @param {string} uri URI to be sanitized.
  69. * @param {string} propName Property name which contained the URI.
  70. * @param {?function(string, string):?goog.html.SafeUrl} uriRewriter A URI
  71. * rewriter that returns a goog.html.SafeUrl.
  72. * @return {?string} Safe URI for use in CSS.
  73. * @private
  74. */
  75. goog.html.sanitizer.CssSanitizer.getSafeUri_ = function(
  76. uri, propName, uriRewriter) {
  77. if (!uriRewriter) {
  78. return null;
  79. }
  80. var safeUri = uriRewriter(uri, propName);
  81. if (safeUri &&
  82. goog.html.SafeUrl.unwrap(safeUri) != goog.html.SafeUrl.INNOCUOUS_STRING) {
  83. return 'url("' +
  84. goog.html.SafeUrl.unwrap(safeUri).replace(
  85. goog.html.sanitizer.CssSanitizer.NORM_URL_REGEXP_,
  86. goog.html.sanitizer.CssSanitizer.normalizeUrlChar_) +
  87. '")';
  88. }
  89. return null;
  90. };
  91. /**
  92. * Used to detect the beginning of the argument list of a CSS property value
  93. * containing a CSS function call.
  94. * @private @const {string}
  95. */
  96. goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_BEGIN_ = '(';
  97. /**
  98. * Used to detect the end of the argument list of a CSS property value
  99. * containing a CSS function call.
  100. * @private @const {string}
  101. */
  102. goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_END_ = ')';
  103. /**
  104. * Allowed CSS functions
  105. * @const @private {!Array<string>}
  106. */
  107. goog.html.sanitizer.CssSanitizer.ALLOWED_FUNCTIONS_ = [
  108. 'rgb',
  109. 'rgba',
  110. 'alpha',
  111. 'rect',
  112. 'image',
  113. 'linear-gradient',
  114. 'radial-gradient',
  115. 'repeating-linear-gradient',
  116. 'repeating-radial-gradient',
  117. 'cubic-bezier',
  118. 'matrix',
  119. 'perspective',
  120. 'rotate',
  121. 'rotate3d',
  122. 'rotatex',
  123. 'rotatey',
  124. 'steps',
  125. 'rotatez',
  126. 'scale',
  127. 'scale3d',
  128. 'scalex',
  129. 'scaley',
  130. 'scalez',
  131. 'skew',
  132. 'skewx',
  133. 'skewy',
  134. 'translate',
  135. 'translate3d',
  136. 'translatex',
  137. 'translatey',
  138. 'translatez'
  139. ];
  140. /**
  141. * Removes a vendor prefix from a property name.
  142. * @param {string} propName A property name.
  143. * @return {string} A property name without vendor prefixes.
  144. * @private
  145. */
  146. goog.html.sanitizer.CssSanitizer.withoutVendorPrefix_ = function(propName) {
  147. // http://stackoverflow.com/a/5411098/20394 has a fairly extensive list
  148. // of vendor prefices. Blink has not declared a vendor prefix distinct from
  149. // -webkit- and http://css-tricks.com/tldr-on-vendor-prefix-drama/ discusses
  150. // how Mozilla recognizes some -webkit- prefixes.
  151. // http://wiki.csswg.org/spec/vendor-prefixes talks more about
  152. // cross-implementation, and lists other prefixes.
  153. return propName.replace(
  154. /^-(?:apple|css|epub|khtml|moz|mso?|o|rim|wap|webkit|xv)-(?=[a-z])/i, '');
  155. };
  156. /**
  157. * Sanitizes the value for a given a browser-parsed CSS value.
  158. * @param {string} propName A property name.
  159. * @param {string} propValue Value of the property as parsed by the browser.
  160. * @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI
  161. * rewriter that returns an unwrapped goog.html.SafeUrl.
  162. * @return {?string} Sanitized property value or null.
  163. * @private
  164. */
  165. goog.html.sanitizer.CssSanitizer.sanitizeProperty_ = function(
  166. propName, propValue, opt_uriRewriter) {
  167. var outputPropValue = goog.string.trim(propValue);
  168. if (outputPropValue == '') {
  169. return null;
  170. }
  171. if (goog.string.caseInsensitiveStartsWith(outputPropValue, 'url(')) {
  172. // Urls are rewritten according to the policy implemented in
  173. // opt_uriRewriter.
  174. // TODO(pelizzi): use HtmlSanitizerUrlPolicy for opt_uriRewriter.
  175. if (!opt_uriRewriter) {
  176. return null;
  177. }
  178. // TODO(danesh): Check if we need to resolve this URI.
  179. var uri = goog.string.stripQuotes(
  180. outputPropValue.substring(4, outputPropValue.length - 1), '"\'');
  181. return goog.html.sanitizer.CssSanitizer.getSafeUri_(
  182. uri, propName, opt_uriRewriter);
  183. } else if (outputPropValue.indexOf('(') > 0) {
  184. // Functions are filtered through a whitelist. Nesting whitelisted functions
  185. // is not supported.
  186. if (goog.string.countOf(
  187. outputPropValue,
  188. goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_BEGIN_) > 1 ||
  189. !(goog.array.contains(
  190. goog.html.sanitizer.CssSanitizer.ALLOWED_FUNCTIONS_,
  191. outputPropValue
  192. .substring(
  193. 0,
  194. outputPropValue.indexOf(goog.html.sanitizer.CssSanitizer
  195. .FUNCTION_ARGUMENTS_BEGIN_))
  196. .toLowerCase()) &&
  197. goog.string.endsWith(
  198. outputPropValue,
  199. goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_END_))) {
  200. // TODO(b/34222379): Handle functions that may need recursing or that may
  201. // appear in the middle of a string. For now, just allow functions which
  202. // aren't nested.
  203. return null;
  204. }
  205. return outputPropValue;
  206. } else {
  207. // Everything else is allowed.
  208. return outputPropValue;
  209. }
  210. };
  211. /**
  212. * Sanitizes an inline style attribute. Short-hand attributes are expanded to
  213. * their individual elements. Note: The sanitizer does not output vendor
  214. * prefixed styles.
  215. * @param {?CSSStyleDeclaration} cssStyle A CSS style object.
  216. * @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI
  217. * rewriter that returns a goog.html.SafeUrl.
  218. * @return {!goog.html.SafeStyle} A sanitized inline cssText.
  219. */
  220. goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle = function(
  221. cssStyle, opt_uriRewriter) {
  222. if (!cssStyle) {
  223. return goog.html.SafeStyle.EMPTY;
  224. }
  225. var cleanCssStyle = document.createElement('div').style;
  226. var cssPropNames =
  227. goog.html.sanitizer.CssSanitizer.getCssPropNames_(cssStyle);
  228. for (var i = 0; i < cssPropNames.length; i++) {
  229. var propName =
  230. goog.html.sanitizer.CssSanitizer.withoutVendorPrefix_(cssPropNames[i]);
  231. if (!goog.html.sanitizer.CssSanitizer.isDisallowedPropertyName_(propName)) {
  232. var propValue =
  233. goog.html.sanitizer.CssSanitizer.getCssValue_(cssStyle, propName);
  234. var sanitizedValue = goog.html.sanitizer.CssSanitizer.sanitizeProperty_(
  235. propName, propValue, opt_uriRewriter);
  236. goog.html.sanitizer.CssSanitizer.setCssValue_(
  237. cleanCssStyle, propName, sanitizedValue);
  238. }
  239. }
  240. return goog.html.uncheckedconversions
  241. .safeStyleFromStringKnownToSatisfyTypeContract(
  242. goog.string.Const.from('Output of CSS sanitizer'),
  243. cleanCssStyle.cssText || '');
  244. };
  245. /**
  246. * Sanitizes inline CSS text and returns it as a SafeStyle object. When adequate
  247. * browser support is not available, such as for IE9 and below, a
  248. * SafeStyle-wrapped empty string is returned.
  249. * @param {string} cssText CSS text to be sanitized.
  250. * @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI
  251. * rewriter that returns a goog.html.SafeUrl.
  252. * @return {!goog.html.SafeStyle} A sanitized inline cssText.
  253. */
  254. goog.html.sanitizer.CssSanitizer.sanitizeInlineStyleString = function(
  255. cssText, opt_uriRewriter) {
  256. // same check as in goog.html.sanitizer.HTML_SANITIZER_SUPPORTED_
  257. if (goog.userAgent.IE && document.documentMode < 10) {
  258. return new goog.html.SafeStyle();
  259. }
  260. var div = goog.html.sanitizer.CssSanitizer
  261. .createInertDocument_()
  262. .createElement('DIV');
  263. div.style.cssText = cssText;
  264. return goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle(
  265. div.style, opt_uriRewriter);
  266. };
  267. /**
  268. * Creates an DOM Document object that will not execute scripts or make
  269. * network requests while parsing HTML.
  270. * @return {!Document}
  271. * @private
  272. */
  273. goog.html.sanitizer.CssSanitizer.createInertDocument_ = function() {
  274. // Documents created using window.document.implementation.createHTMLDocument()
  275. // use the same custom component registry as their parent document. This means
  276. // that parsing arbitrary HTML can result in calls to user-defined JavaScript.
  277. // This is worked around by creating a template element and its content's
  278. // document. See https://github.com/cure53/DOMPurify/issues/47.
  279. var doc = document;
  280. if (typeof HTMLTemplateElement === 'function') {
  281. doc =
  282. goog.dom.createElement(goog.dom.TagName.TEMPLATE).content.ownerDocument;
  283. }
  284. return doc.implementation.createHTMLDocument('');
  285. };
  286. /**
  287. * Provides a cross-browser way to get a CSS property names.
  288. * @param {!CSSStyleDeclaration} cssStyle A CSS style object.
  289. * @return {!Array<string>} CSS property names.
  290. * @private
  291. */
  292. goog.html.sanitizer.CssSanitizer.getCssPropNames_ = function(cssStyle) {
  293. var propNames = [];
  294. if (goog.isArrayLike(cssStyle)) {
  295. // Gets property names via item().
  296. // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-item
  297. propNames = goog.array.toArray(cssStyle);
  298. } else {
  299. // In IE8 and other older browsers we have to iterate over all the property
  300. // names. We skip cssText because it contains the unsanitized CSS, which
  301. // defeats the purpose.
  302. propNames = goog.object.getKeys(cssStyle);
  303. goog.array.remove(propNames, 'cssText');
  304. }
  305. return propNames;
  306. };
  307. /**
  308. * Provides a way to get a CSS value without falling prey to things like
  309. * &lt;form&gt;&lt;input name="propertyValue"&gt;
  310. * &lt;input name="propertyValue"&gt;&lt;/form&gt;. If not available,
  311. * likely only older browsers, fallback to a direct call.
  312. * @param {!CSSStyleDeclaration} cssStyle A CSS style object.
  313. * @param {string} propName A property name.
  314. * @return {string} Value of the property as parsed by the browser.
  315. * @private
  316. */
  317. goog.html.sanitizer.CssSanitizer.getCssValue_ = function(cssStyle, propName) {
  318. var getPropDescriptor = Object.getOwnPropertyDescriptor(
  319. CSSStyleDeclaration.prototype, 'getPropertyValue');
  320. if (getPropDescriptor && cssStyle.getPropertyValue) {
  321. // getPropertyValue on Safari can return null
  322. return getPropDescriptor.value.call(cssStyle, propName) || '';
  323. } else if (cssStyle.getAttribute) {
  324. // In IE8 and other older browers we make a direct call to getAttribute.
  325. return String(cssStyle.getAttribute(propName) || '');
  326. } else {
  327. // Unsupported, likely quite old, browser.
  328. return '';
  329. }
  330. };
  331. /**
  332. * Provides a way to set a CSS value without falling prey to things like
  333. * &lt;form&gt;&lt;input name="property"&gt;
  334. * &lt;input name="property"&gt;&lt;/form&gt;. If not available,
  335. * likely only older browsers, fallback to a direct call.
  336. * @param {!CSSStyleDeclaration} cssStyle A CSS style object.
  337. * @param {string} propName A property name.
  338. * @param {?string} sanitizedValue Sanitized value of the property to be set
  339. * on the CSS style object.
  340. * @private
  341. */
  342. goog.html.sanitizer.CssSanitizer.setCssValue_ = function(
  343. cssStyle, propName, sanitizedValue) {
  344. if (sanitizedValue) {
  345. var setPropDescriptor = Object.getOwnPropertyDescriptor(
  346. CSSStyleDeclaration.prototype, 'setProperty');
  347. if (setPropDescriptor && cssStyle.setProperty) {
  348. setPropDescriptor.value.call(cssStyle, propName, sanitizedValue);
  349. } else if (cssStyle.setAttribute) {
  350. // In IE8 and other older browers we make a direct call to setAttribute.
  351. cssStyle.setAttribute(propName, sanitizedValue);
  352. }
  353. }
  354. };
  355. /**
  356. * Checks whether the property name specified should be disallowed.
  357. * @param {string} propName A property name.
  358. * @return {boolean} Whether the property name is disallowed.
  359. * @private
  360. */
  361. goog.html.sanitizer.CssSanitizer.isDisallowedPropertyName_ = function(
  362. propName) {
  363. // getPropertyValue doesn't deal with custom variables properly and will NOT
  364. // decode CSS escapes (but the browser will do so silently). Simply disallow
  365. // custom variables (http://www.w3.org/TR/css-variables/#defining-variables).
  366. return goog.string.startsWith(propName, '--') ||
  367. goog.string.startsWith(propName, 'var');
  368. };