Path: blob/trunk/third_party/closure/goog/html/sanitizer/csssanitizer.js
2868 views
// Copyright 2016 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview16* JavaScript support for client-side CSS sanitization.17*18* @author [email protected] (Danesh Irani)19* @author [email protected] (Mike Samuel)20*/2122goog.provide('goog.html.sanitizer.CssSanitizer');2324goog.require('goog.array');25goog.require('goog.dom');26goog.require('goog.dom.TagName');27goog.require('goog.html.SafeStyle');28goog.require('goog.html.SafeUrl');29goog.require('goog.html.uncheckedconversions');30goog.require('goog.object');31goog.require('goog.string');323334/**35* The set of characters that need to be normalized inside url("...").36* We normalize newlines because they are not allowed inside quoted strings,37* normalize quote characters, angle-brackets, and asterisks because they38* could be used to break out of the URL or introduce targets for CSS39* error recovery. We normalize parentheses since they delimit unquoted40* URLs and calls and could be a target for error recovery.41* @const @private {!RegExp}42*/43goog.html.sanitizer.CssSanitizer.NORM_URL_REGEXP_ = /[\n\f\r\"\'()*<>]/g;444546/**47* The replacements for NORM_URL_REGEXP.48* @private @const {!Object<string, string>}49*/50goog.html.sanitizer.CssSanitizer.NORM_URL_REPLACEMENTS_ = {51'\n': '%0a',52'\f': '%0c',53'\r': '%0d',54'"': '%22',55'\'': '%27',56'(': '%28',57')': '%29',58'*': '%2a',59'<': '%3c',60'>': '%3e'61};626364/**65* Normalizes a character for use in a url() directive.66* @param {string} ch Character to be normalized.67* @return {?string} Normalized character.68* @private69*/70goog.html.sanitizer.CssSanitizer.normalizeUrlChar_ = function(ch) {71return goog.html.sanitizer.CssSanitizer.NORM_URL_REPLACEMENTS_[ch] || null;72};737475/**76* Constructs a safe URI from a given URI and prop using a given uriRewriter77* function.78* @param {string} uri URI to be sanitized.79* @param {string} propName Property name which contained the URI.80* @param {?function(string, string):?goog.html.SafeUrl} uriRewriter A URI81* rewriter that returns a goog.html.SafeUrl.82* @return {?string} Safe URI for use in CSS.83* @private84*/85goog.html.sanitizer.CssSanitizer.getSafeUri_ = function(86uri, propName, uriRewriter) {87if (!uriRewriter) {88return null;89}90var safeUri = uriRewriter(uri, propName);91if (safeUri &&92goog.html.SafeUrl.unwrap(safeUri) != goog.html.SafeUrl.INNOCUOUS_STRING) {93return 'url("' +94goog.html.SafeUrl.unwrap(safeUri).replace(95goog.html.sanitizer.CssSanitizer.NORM_URL_REGEXP_,96goog.html.sanitizer.CssSanitizer.normalizeUrlChar_) +97'")';98}99return null;100};101102103/**104* Used to detect the beginning of the argument list of a CSS property value105* containing a CSS function call.106* @private @const {string}107*/108goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_BEGIN_ = '(';109110111/**112* Used to detect the end of the argument list of a CSS property value113* containing a CSS function call.114* @private @const {string}115*/116goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_END_ = ')';117118119/**120* Allowed CSS functions121* @const @private {!Array<string>}122*/123goog.html.sanitizer.CssSanitizer.ALLOWED_FUNCTIONS_ = [124'rgb',125'rgba',126'alpha',127'rect',128'image',129'linear-gradient',130'radial-gradient',131'repeating-linear-gradient',132'repeating-radial-gradient',133'cubic-bezier',134'matrix',135'perspective',136'rotate',137'rotate3d',138'rotatex',139'rotatey',140'steps',141'rotatez',142'scale',143'scale3d',144'scalex',145'scaley',146'scalez',147'skew',148'skewx',149'skewy',150'translate',151'translate3d',152'translatex',153'translatey',154'translatez'155];156157158/**159* Removes a vendor prefix from a property name.160* @param {string} propName A property name.161* @return {string} A property name without vendor prefixes.162* @private163*/164goog.html.sanitizer.CssSanitizer.withoutVendorPrefix_ = function(propName) {165// http://stackoverflow.com/a/5411098/20394 has a fairly extensive list166// of vendor prefices. Blink has not declared a vendor prefix distinct from167// -webkit- and http://css-tricks.com/tldr-on-vendor-prefix-drama/ discusses168// how Mozilla recognizes some -webkit- prefixes.169// http://wiki.csswg.org/spec/vendor-prefixes talks more about170// cross-implementation, and lists other prefixes.171return propName.replace(172/^-(?:apple|css|epub|khtml|moz|mso?|o|rim|wap|webkit|xv)-(?=[a-z])/i, '');173};174175176/**177* Sanitizes the value for a given a browser-parsed CSS value.178* @param {string} propName A property name.179* @param {string} propValue Value of the property as parsed by the browser.180* @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI181* rewriter that returns an unwrapped goog.html.SafeUrl.182* @return {?string} Sanitized property value or null.183* @private184*/185goog.html.sanitizer.CssSanitizer.sanitizeProperty_ = function(186propName, propValue, opt_uriRewriter) {187var outputPropValue = goog.string.trim(propValue);188if (outputPropValue == '') {189return null;190}191192if (goog.string.caseInsensitiveStartsWith(outputPropValue, 'url(')) {193// Urls are rewritten according to the policy implemented in194// opt_uriRewriter.195// TODO(pelizzi): use HtmlSanitizerUrlPolicy for opt_uriRewriter.196if (!opt_uriRewriter) {197return null;198}199// TODO(danesh): Check if we need to resolve this URI.200var uri = goog.string.stripQuotes(201outputPropValue.substring(4, outputPropValue.length - 1), '"\'');202203return goog.html.sanitizer.CssSanitizer.getSafeUri_(204uri, propName, opt_uriRewriter);205} else if (outputPropValue.indexOf('(') > 0) {206// Functions are filtered through a whitelist. Nesting whitelisted functions207// is not supported.208if (goog.string.countOf(209outputPropValue,210goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_BEGIN_) > 1 ||211!(goog.array.contains(212goog.html.sanitizer.CssSanitizer.ALLOWED_FUNCTIONS_,213outputPropValue214.substring(2150,216outputPropValue.indexOf(goog.html.sanitizer.CssSanitizer217.FUNCTION_ARGUMENTS_BEGIN_))218.toLowerCase()) &&219goog.string.endsWith(220outputPropValue,221goog.html.sanitizer.CssSanitizer.FUNCTION_ARGUMENTS_END_))) {222// TODO(b/34222379): Handle functions that may need recursing or that may223// appear in the middle of a string. For now, just allow functions which224// aren't nested.225return null;226}227return outputPropValue;228} else {229// Everything else is allowed.230return outputPropValue;231}232};233234235/**236* Sanitizes an inline style attribute. Short-hand attributes are expanded to237* their individual elements. Note: The sanitizer does not output vendor238* prefixed styles.239* @param {?CSSStyleDeclaration} cssStyle A CSS style object.240* @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI241* rewriter that returns a goog.html.SafeUrl.242* @return {!goog.html.SafeStyle} A sanitized inline cssText.243*/244goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle = function(245cssStyle, opt_uriRewriter) {246if (!cssStyle) {247return goog.html.SafeStyle.EMPTY;248}249250var cleanCssStyle = document.createElement('div').style;251var cssPropNames =252goog.html.sanitizer.CssSanitizer.getCssPropNames_(cssStyle);253254for (var i = 0; i < cssPropNames.length; i++) {255var propName =256goog.html.sanitizer.CssSanitizer.withoutVendorPrefix_(cssPropNames[i]);257if (!goog.html.sanitizer.CssSanitizer.isDisallowedPropertyName_(propName)) {258var propValue =259goog.html.sanitizer.CssSanitizer.getCssValue_(cssStyle, propName);260261var sanitizedValue = goog.html.sanitizer.CssSanitizer.sanitizeProperty_(262propName, propValue, opt_uriRewriter);263goog.html.sanitizer.CssSanitizer.setCssValue_(264cleanCssStyle, propName, sanitizedValue);265}266}267return goog.html.uncheckedconversions268.safeStyleFromStringKnownToSatisfyTypeContract(269goog.string.Const.from('Output of CSS sanitizer'),270cleanCssStyle.cssText || '');271};272273274/**275* Sanitizes inline CSS text and returns it as a SafeStyle object. When adequate276* browser support is not available, such as for IE9 and below, a277* SafeStyle-wrapped empty string is returned.278* @param {string} cssText CSS text to be sanitized.279* @param {function(string, string):?goog.html.SafeUrl=} opt_uriRewriter A URI280* rewriter that returns a goog.html.SafeUrl.281* @return {!goog.html.SafeStyle} A sanitized inline cssText.282*/283goog.html.sanitizer.CssSanitizer.sanitizeInlineStyleString = function(284cssText, opt_uriRewriter) {285// same check as in goog.html.sanitizer.HTML_SANITIZER_SUPPORTED_286if (goog.userAgent.IE && document.documentMode < 10) {287return new goog.html.SafeStyle();288}289290var div = goog.html.sanitizer.CssSanitizer291.createInertDocument_()292.createElement('DIV');293div.style.cssText = cssText;294return goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle(295div.style, opt_uriRewriter);296};297298299/**300* Creates an DOM Document object that will not execute scripts or make301* network requests while parsing HTML.302* @return {!Document}303* @private304*/305goog.html.sanitizer.CssSanitizer.createInertDocument_ = function() {306// Documents created using window.document.implementation.createHTMLDocument()307// use the same custom component registry as their parent document. This means308// that parsing arbitrary HTML can result in calls to user-defined JavaScript.309// This is worked around by creating a template element and its content's310// document. See https://github.com/cure53/DOMPurify/issues/47.311var doc = document;312if (typeof HTMLTemplateElement === 'function') {313doc =314goog.dom.createElement(goog.dom.TagName.TEMPLATE).content.ownerDocument;315}316return doc.implementation.createHTMLDocument('');317};318319320/**321* Provides a cross-browser way to get a CSS property names.322* @param {!CSSStyleDeclaration} cssStyle A CSS style object.323* @return {!Array<string>} CSS property names.324* @private325*/326goog.html.sanitizer.CssSanitizer.getCssPropNames_ = function(cssStyle) {327var propNames = [];328if (goog.isArrayLike(cssStyle)) {329// Gets property names via item().330// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-item331propNames = goog.array.toArray(cssStyle);332} else {333// In IE8 and other older browsers we have to iterate over all the property334// names. We skip cssText because it contains the unsanitized CSS, which335// defeats the purpose.336propNames = goog.object.getKeys(cssStyle);337goog.array.remove(propNames, 'cssText');338}339return propNames;340};341342343/**344* Provides a way to get a CSS value without falling prey to things like345* <form><input name="propertyValue">346* <input name="propertyValue"></form>. If not available,347* likely only older browsers, fallback to a direct call.348* @param {!CSSStyleDeclaration} cssStyle A CSS style object.349* @param {string} propName A property name.350* @return {string} Value of the property as parsed by the browser.351* @private352*/353goog.html.sanitizer.CssSanitizer.getCssValue_ = function(cssStyle, propName) {354var getPropDescriptor = Object.getOwnPropertyDescriptor(355CSSStyleDeclaration.prototype, 'getPropertyValue');356if (getPropDescriptor && cssStyle.getPropertyValue) {357// getPropertyValue on Safari can return null358return getPropDescriptor.value.call(cssStyle, propName) || '';359} else if (cssStyle.getAttribute) {360// In IE8 and other older browers we make a direct call to getAttribute.361return String(cssStyle.getAttribute(propName) || '');362} else {363// Unsupported, likely quite old, browser.364return '';365}366};367368369/**370* Provides a way to set a CSS value without falling prey to things like371* <form><input name="property">372* <input name="property"></form>. If not available,373* likely only older browsers, fallback to a direct call.374* @param {!CSSStyleDeclaration} cssStyle A CSS style object.375* @param {string} propName A property name.376* @param {?string} sanitizedValue Sanitized value of the property to be set377* on the CSS style object.378* @private379*/380goog.html.sanitizer.CssSanitizer.setCssValue_ = function(381cssStyle, propName, sanitizedValue) {382if (sanitizedValue) {383var setPropDescriptor = Object.getOwnPropertyDescriptor(384CSSStyleDeclaration.prototype, 'setProperty');385if (setPropDescriptor && cssStyle.setProperty) {386setPropDescriptor.value.call(cssStyle, propName, sanitizedValue);387} else if (cssStyle.setAttribute) {388// In IE8 and other older browers we make a direct call to setAttribute.389cssStyle.setAttribute(propName, sanitizedValue);390}391}392};393394395/**396* Checks whether the property name specified should be disallowed.397* @param {string} propName A property name.398* @return {boolean} Whether the property name is disallowed.399* @private400*/401goog.html.sanitizer.CssSanitizer.isDisallowedPropertyName_ = function(402propName) {403// getPropertyValue doesn't deal with custom variables properly and will NOT404// decode CSS escapes (but the browser will do so silently). Simply disallow405// custom variables (http://www.w3.org/TR/css-variables/#defining-variables).406return goog.string.startsWith(propName, '--') ||407goog.string.startsWith(propName, 'var');408};409410411