accessibility-developer-tools
Version:
This is a library of accessibility-related testing and utility code.
376 lines (338 loc) • 12.8 kB
JavaScript
// 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.html.SafeStyle');
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):?string} uriRewriter A URI rewriter that
* returns an unwrapped 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) {
return 'url("' +
safeUri.replace(
goog.html.sanitizer.CssSanitizer.NORM_URL_REGEXP_,
goog.html.sanitizer.CssSanitizer.normalizeUrlChar_) +
'")';
}
return null;
};
/**
* 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, '');
};
/**
* Given a browser-parsed CSS value sanitizes the value.
* @param {string} propName A property name.
* @param {string} propValue Value of the property as parsed by the browser.
* @param {function(string, string)=} 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 propertyValue = goog.string.trim(propValue).toLowerCase();
if (propertyValue === '') {
return null;
}
if (goog.string.startsWith(propertyValue, 'url(')) {
// Handle url("...") by rewriting the body.
if (opt_uriRewriter) {
// Preserve original case
propertyValue = goog.string.trim(propValue);
// TODO(danesh): Check if we need to resolve this Uri.
var uri = goog.string.stripQuotes(
propertyValue.substring(4, propertyValue.length - 1), '"\'');
propertyValue = goog.html.sanitizer.CssSanitizer.getSafeUri_(
uri, propName, opt_uriRewriter);
} else {
propertyValue = null;
}
} else if (propertyValue.indexOf('(') > 0) {
if (goog.string.countOf(propertyValue, '(') > 1 ||
!(goog.array.contains(
goog.html.sanitizer.CssSanitizer.ALLOWED_FUNCTIONS_,
propertyValue.substring(0, propertyValue.indexOf('('))) &&
goog.string.endsWith(propertyValue, ')'))) {
// Functions start at a token like "name(" and end with a ")" taking
// into account nesting.
// TODO(danesh): 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.
propertyValue = null;
}
}
return propertyValue;
};
/**
* 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)=} opt_uriRewriter A URI rewriter that
* returns an unwrapped 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)=} opt_uriRewriter A URI rewriter that
* returns an unwrapped 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 = document.createElement('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 = Array.prototype.slice.call(cssStyle);
} else {
// In IE8 and other older browers we have to iterate over all the property
// names.
propNames = goog.object.getKeys(cssStyle);
}
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');
};