123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- // 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
- * JavaScript support for client-side CSS sanitization.
- *
- * @author danesh@google.com (Danesh Irani)
- * @author mikesamuel@gmail.com (Mike Samuel)
- */
- goog.provide('goog.html.sanitizer.CssSanitizer');
- goog.require('goog.array');
- goog.require('goog.dom');
- goog.require('goog.dom.TagName');
- goog.require('goog.html.SafeStyle');
- goog.require('goog.html.SafeUrl');
- goog.require('goog.html.uncheckedconversions');
- goog.require('goog.object');
- goog.require('goog.string');
- /**
- * The set of characters that need to be normalized inside url("...").
- * We normalize newlines because they are not allowed inside quoted strings,
- * normalize quote characters, angle-brackets, and asterisks because they
- * could be used to break out of the URL or introduce targets for CSS
- * error recovery. We normalize parentheses since they delimit unquoted
- * URLs and calls and could be a target for error recovery.
- * @const @private {!RegExp}
- */
- goog.html.sanitizer.CssSanitizer.NORM_URL_REGEXP_ = /[\n\f\r\"\'()*<>]/g;
- /**
- * The replacements for NORM_URL_REGEXP.
- * @private @const {!Object<string, string>}
- */
- goog.html.sanitizer.CssSanitizer.NORM_URL_REPLACEMENTS_ = {
- '\n': '%0a',
- '\f': '%0c',
- '\r': '%0d',
- '"': '%22',
- '\'': '%27',
- '(': '%28',
- ')': '%29',
- '*': '%2a',
- '<': '%3c',
- '>': '%3e'
- };
- /**
- * Normalizes a character for use in a url() directive.
- * @param {string} ch Character to be normalized.
- * @return {?string} Normalized character.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.normalizeUrlChar_ = function(ch) {
- return goog.html.sanitizer.CssSanitizer.NORM_URL_REPLACEMENTS_[ch] || null;
- };
- /**
- * Constructs a safe URI from a given URI and prop using a given uriRewriter
- * function.
- * @param {string} uri URI to be sanitized.
- * @param {string} propName Property name which contained the URI.
- * @param {?function(string, string):?goog.html.SafeUrl} uriRewriter A URI
- * rewriter that returns a goog.html.SafeUrl.
- * @return {?string} Safe URI for use in CSS.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.getSafeUri_ = function(
- uri, propName, uriRewriter) {
- if (!uriRewriter) {
- return null;
- }
- var safeUri = uriRewriter(uri, propName);
- if (safeUri &&
- goog.html.SafeUrl.unwrap(safeUri) != goog.html.SafeUrl.INNOCUOUS_STRING) {
- return 'url("' +
- goog.html.SafeUrl.unwrap(safeUri).replace(
- goog.html.sanitizer.CssSanitizer.NORM_URL_REGEXP_,
- goog.html.sanitizer.CssSanitizer.normalizeUrlChar_) +
- '")';
- }
- return null;
- };
- /**
- * Used to detect the beginning of the argument list of a CSS property value
- * containing a CSS function call.
- * @private @const {string}
- */
- goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_BEGIN_ = '(';
- /**
- * Used to detect the end of the argument list of a CSS property value
- * containing a CSS function call.
- * @private @const {string}
- */
- goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_END_ = ')';
- /**
- * Allowed CSS functions
- * @const @private {!Array<string>}
- */
- goog.html.sanitizer.CssSanitizer.ALLOWED_FUNCTIONS_ = [
- 'rgb',
- 'rgba',
- 'alpha',
- 'rect',
- 'image',
- 'linear-gradient',
- 'radial-gradient',
- 'repeating-linear-gradient',
- 'repeating-radial-gradient',
- 'cubic-bezier',
- 'matrix',
- 'perspective',
- 'rotate',
- 'rotate3d',
- 'rotatex',
- 'rotatey',
- 'steps',
- 'rotatez',
- 'scale',
- 'scale3d',
- 'scalex',
- 'scaley',
- 'scalez',
- 'skew',
- 'skewx',
- 'skewy',
- 'translate',
- 'translate3d',
- 'translatex',
- 'translatey',
- 'translatez'
- ];
- /**
- * Removes a vendor prefix from a property name.
- * @param {string} propName A property name.
- * @return {string} A property name without vendor prefixes.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.withoutVendorPrefix_ = function(propName) {
- // http://stackoverflow.com/a/5411098/20394 has a fairly extensive list
- // of vendor prefices. Blink has not declared a vendor prefix distinct from
- // -webkit- and http://css-tricks.com/tldr-on-vendor-prefix-drama/ discusses
- // how Mozilla recognizes some -webkit- prefixes.
- // http://wiki.csswg.org/spec/vendor-prefixes talks more about
- // cross-implementation, and lists other prefixes.
- return propName.replace(
- /^-(?:apple|css|epub|khtml|moz|mso?|o|rim|wap|webkit|xv)-(?=[a-z])/i, '');
- };
- /**
- * Sanitizes the value for a given a browser-parsed CSS value.
- * @param {string} propName A property name.
- * @param {string} propValue Value of the property as parsed by the browser.
- * @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI
- * rewriter that returns an unwrapped goog.html.SafeUrl.
- * @return {?string} Sanitized property value or null.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.sanitizeProperty_ = function(
- propName, propValue, opt_uriRewriter) {
- var outputPropValue = goog.string.trim(propValue);
- if (outputPropValue == '') {
- return null;
- }
- if (goog.string.caseInsensitiveStartsWith(outputPropValue, 'url(')) {
- // Urls are rewritten according to the policy implemented in
- // opt_uriRewriter.
- // TODO(pelizzi): use HtmlSanitizerUrlPolicy for opt_uriRewriter.
- if (!opt_uriRewriter) {
- return null;
- }
- // TODO(danesh): Check if we need to resolve this URI.
- var uri = goog.string.stripQuotes(
- outputPropValue.substring(4, outputPropValue.length - 1), '"\'');
- return goog.html.sanitizer.CssSanitizer.getSafeUri_(
- uri, propName, opt_uriRewriter);
- } else if (outputPropValue.indexOf('(') > 0) {
- // Functions are filtered through a whitelist. Nesting whitelisted functions
- // is not supported.
- if (goog.string.countOf(
- outputPropValue,
- goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_BEGIN_) > 1 ||
- !(goog.array.contains(
- goog.html.sanitizer.CssSanitizer.ALLOWED_FUNCTIONS_,
- outputPropValue
- .substring(
- 0,
- outputPropValue.indexOf(goog.html.sanitizer.CssSanitizer
- .FUNCTION_ARGUMENTS_BEGIN_))
- .toLowerCase()) &&
- goog.string.endsWith(
- outputPropValue,
- goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_END_))) {
- // TODO(b/34222379): Handle functions that may need recursing or that may
- // appear in the middle of a string. For now, just allow functions which
- // aren't nested.
- return null;
- }
- return outputPropValue;
- } else {
- // Everything else is allowed.
- return outputPropValue;
- }
- };
- /**
- * Sanitizes an inline style attribute. Short-hand attributes are expanded to
- * their individual elements. Note: The sanitizer does not output vendor
- * prefixed styles.
- * @param {?CSSStyleDeclaration} cssStyle A CSS style object.
- * @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI
- * rewriter that returns a goog.html.SafeUrl.
- * @return {!goog.html.SafeStyle} A sanitized inline cssText.
- */
- goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle = function(
- cssStyle, opt_uriRewriter) {
- if (!cssStyle) {
- return goog.html.SafeStyle.EMPTY;
- }
- var cleanCssStyle = document.createElement('div').style;
- var cssPropNames =
- goog.html.sanitizer.CssSanitizer.getCssPropNames_(cssStyle);
- for (var i = 0; i < cssPropNames.length; i++) {
- var propName =
- goog.html.sanitizer.CssSanitizer.withoutVendorPrefix_(cssPropNames[i]);
- if (!goog.html.sanitizer.CssSanitizer.isDisallowedPropertyName_(propName)) {
- var propValue =
- goog.html.sanitizer.CssSanitizer.getCssValue_(cssStyle, propName);
- var sanitizedValue = goog.html.sanitizer.CssSanitizer.sanitizeProperty_(
- propName, propValue, opt_uriRewriter);
- goog.html.sanitizer.CssSanitizer.setCssValue_(
- cleanCssStyle, propName, sanitizedValue);
- }
- }
- return goog.html.uncheckedconversions
- .safeStyleFromStringKnownToSatisfyTypeContract(
- goog.string.Const.from('Output of CSS sanitizer'),
- cleanCssStyle.cssText || '');
- };
- /**
- * Sanitizes inline CSS text and returns it as a SafeStyle object. When adequate
- * browser support is not available, such as for IE9 and below, a
- * SafeStyle-wrapped empty string is returned.
- * @param {string} cssText CSS text to be sanitized.
- * @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI
- * rewriter that returns a goog.html.SafeUrl.
- * @return {!goog.html.SafeStyle} A sanitized inline cssText.
- */
- goog.html.sanitizer.CssSanitizer.sanitizeInlineStyleString = function(
- cssText, opt_uriRewriter) {
- // same check as in goog.html.sanitizer.HTML_SANITIZER_SUPPORTED_
- if (goog.userAgent.IE && document.documentMode < 10) {
- return new goog.html.SafeStyle();
- }
- var div = goog.html.sanitizer.CssSanitizer
- .createInertDocument_()
- .createElement('DIV');
- div.style.cssText = cssText;
- return goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle(
- div.style, opt_uriRewriter);
- };
- /**
- * Creates an DOM Document object that will not execute scripts or make
- * network requests while parsing HTML.
- * @return {!Document}
- * @private
- */
- goog.html.sanitizer.CssSanitizer.createInertDocument_ = function() {
- // Documents created using window.document.implementation.createHTMLDocument()
- // use the same custom component registry as their parent document. This means
- // that parsing arbitrary HTML can result in calls to user-defined JavaScript.
- // This is worked around by creating a template element and its content's
- // document. See https://github.com/cure53/DOMPurify/issues/47.
- var doc = document;
- if (typeof HTMLTemplateElement === 'function') {
- doc =
- goog.dom.createElement(goog.dom.TagName.TEMPLATE).content.ownerDocument;
- }
- return doc.implementation.createHTMLDocument('');
- };
- /**
- * Provides a cross-browser way to get a CSS property names.
- * @param {!CSSStyleDeclaration} cssStyle A CSS style object.
- * @return {!Array<string>} CSS property names.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.getCssPropNames_ = function(cssStyle) {
- var propNames = [];
- if (goog.isArrayLike(cssStyle)) {
- // Gets property names via item().
- // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-item
- propNames = goog.array.toArray(cssStyle);
- } else {
- // In IE8 and other older browsers we have to iterate over all the property
- // names. We skip cssText because it contains the unsanitized CSS, which
- // defeats the purpose.
- propNames = goog.object.getKeys(cssStyle);
- goog.array.remove(propNames, 'cssText');
- }
- return propNames;
- };
- /**
- * Provides a way to get a CSS value without falling prey to things like
- * <form><input name="propertyValue">
- * <input name="propertyValue"></form>. If not available,
- * likely only older browsers, fallback to a direct call.
- * @param {!CSSStyleDeclaration} cssStyle A CSS style object.
- * @param {string} propName A property name.
- * @return {string} Value of the property as parsed by the browser.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.getCssValue_ = function(cssStyle, propName) {
- var getPropDescriptor = Object.getOwnPropertyDescriptor(
- CSSStyleDeclaration.prototype, 'getPropertyValue');
- if (getPropDescriptor && cssStyle.getPropertyValue) {
- // getPropertyValue on Safari can return null
- return getPropDescriptor.value.call(cssStyle, propName) || '';
- } else if (cssStyle.getAttribute) {
- // In IE8 and other older browers we make a direct call to getAttribute.
- return String(cssStyle.getAttribute(propName) || '');
- } else {
- // Unsupported, likely quite old, browser.
- return '';
- }
- };
- /**
- * Provides a way to set a CSS value without falling prey to things like
- * <form><input name="property">
- * <input name="property"></form>. If not available,
- * likely only older browsers, fallback to a direct call.
- * @param {!CSSStyleDeclaration} cssStyle A CSS style object.
- * @param {string} propName A property name.
- * @param {?string} sanitizedValue Sanitized value of the property to be set
- * on the CSS style object.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.setCssValue_ = function(
- cssStyle, propName, sanitizedValue) {
- if (sanitizedValue) {
- var setPropDescriptor = Object.getOwnPropertyDescriptor(
- CSSStyleDeclaration.prototype, 'setProperty');
- if (setPropDescriptor && cssStyle.setProperty) {
- setPropDescriptor.value.call(cssStyle, propName, sanitizedValue);
- } else if (cssStyle.setAttribute) {
- // In IE8 and other older browers we make a direct call to setAttribute.
- cssStyle.setAttribute(propName, sanitizedValue);
- }
- }
- };
- /**
- * Checks whether the property name specified should be disallowed.
- * @param {string} propName A property name.
- * @return {boolean} Whether the property name is disallowed.
- * @private
- */
- goog.html.sanitizer.CssSanitizer.isDisallowedPropertyName_ = function(
- propName) {
- // getPropertyValue doesn't deal with custom variables properly and will NOT
- // decode CSS escapes (but the browser will do so silently). Simply disallow
- // custom variables (http://www.w3.org/TR/css-variables/#defining-variables).
- return goog.string.startsWith(propName, '--') ||
- goog.string.startsWith(propName, 'var');
- };
|