Path: blob/trunk/third_party/closure/goog/html/safestyle.js
4143 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview The SafeStyle type and its builders.8*9* TODO(xtof): Link to document stating type contract.10*/1112goog.module('goog.html.SafeStyle');13goog.module.declareLegacyNamespace();1415const Const = goog.require('goog.string.Const');16const SafeUrl = goog.require('goog.html.SafeUrl');17const TypedString = goog.require('goog.string.TypedString');18const {AssertionError, assert, fail} = goog.require('goog.asserts');19const {contains, endsWith} = goog.require('goog.string.internal');20const utils = goog.require('goog.utils');2122/**23* Token used to ensure that object is created only from this file. No code24* outside of this file can access this token.25* @type {!Object}26* @const27*/28const CONSTRUCTOR_TOKEN_PRIVATE = {};2930/**31* A string-like object which represents a sequence of CSS declarations32* (`propertyName1: propertyvalue1; propertyName2: propertyValue2; ...`)33* and that carries the security type contract that its value, as a string,34* will not cause untrusted script execution (XSS) when evaluated as CSS in a35* browser.36*37* Instances of this type must be created via the factory methods38* (`SafeStyle.create` or `SafeStyle.fromConstant`)39* and not by invoking its constructor. The constructor intentionally takes an40* extra parameter that cannot be constructed outside of this file and the type41* is immutable; hence only a default instance corresponding to the empty string42* can be obtained via constructor invocation.43*44* SafeStyle's string representation can safely be:45* <ul>46* <li>Interpolated as the content of a *quoted* HTML style attribute.47* However, the SafeStyle string *must be HTML-attribute-escaped* before48* interpolation.49* <li>Interpolated as the content of a {}-wrapped block within a stylesheet.50* '<' characters in the SafeStyle string *must be CSS-escaped* before51* interpolation. The SafeStyle string is also guaranteed not to be able52* to introduce new properties or elide existing ones.53* <li>Interpolated as the content of a {}-wrapped block within an HTML54* <style> element. '<' characters in the SafeStyle string55* *must be CSS-escaped* before interpolation.56* <li>Assigned to the style property of a DOM node. The SafeStyle string57* should not be escaped before being assigned to the property.58* </ul>59*60* A SafeStyle may never contain literal angle brackets. Otherwise, it could61* be unsafe to place a SafeStyle into a <style> tag (where it can't62* be HTML escaped). For example, if the SafeStyle containing63* `font: 'foo <style/><script>evil</script>'` were64* interpolated within a <style> tag, this would then break out of the65* style context into HTML.66*67* A SafeStyle may contain literal single or double quotes, and as such the68* entire style string must be escaped when used in a style attribute (if69* this were not the case, the string could contain a matching quote that70* would escape from the style attribute).71*72* Values of this type must be composable, i.e. for any two values73* `style1` and `style2` of this type,74* `SafeStyle.unwrap(style1) +75* SafeStyle.unwrap(style2)` must itself be a value that satisfies76* the SafeStyle type constraint. This requirement implies that for any value77* `style` of this type, `SafeStyle.unwrap(style)` must78* not end in a "property value" or "property name" context. For example,79* a value of `background:url("` or `font-` would not satisfy the80* SafeStyle contract. This is because concatenating such strings with a81* second value that itself does not contain unsafe CSS can result in an82* overall string that does. For example, if `javascript:evil())"` is83* appended to `background:url("}, the resulting string may result in84* the execution of a malicious script.85*86* TODO(mlourenco): Consider whether we should implement UTF-8 interchange87* validity checks and blacklisting of newlines (including Unicode ones) and88* other whitespace characters (\t, \f). Document here if so and also update89* SafeStyle.fromConstant().90*91* The following example values comply with this type's contract:92* <ul>93* <li><pre>width: 1em;</pre>94* <li><pre>height:1em;</pre>95* <li><pre>width: 1em;height: 1em;</pre>96* <li><pre>background:url('http://url');</pre>97* </ul>98* In addition, the empty string is safe for use in a CSS attribute.99*100* The following example values do NOT comply with this type's contract:101* <ul>102* <li><pre>background: red</pre> (missing a trailing semi-colon)103* <li><pre>background:</pre> (missing a value and a trailing semi-colon)104* <li><pre>1em</pre> (missing an attribute name, which provides context for105* the value)106* </ul>107*108* @see SafeStyle#create109* @see SafeStyle#fromConstant110* @see http://www.w3.org/TR/css3-syntax/111* @final112* @struct113* @implements {TypedString}114*/115class SafeStyle {116/**117* @param {string} value118* @param {!Object} token package-internal implementation detail.119*/120constructor(value, token) {121if (goog.DEBUG && token !== CONSTRUCTOR_TOKEN_PRIVATE) {122throw Error('SafeStyle is not meant to be built directly');123}124125/**126* The contained value of this SafeStyle. The field has a purposely127* ugly name to make (non-compiled) code that attempts to directly access128* this field stand out.129* @const130* @private {string}131*/132this.privateDoNotAccessOrElseSafeStyleWrappedValue_ = value;133134/**135* @override136* @const {boolean}137*/138this.implementsGoogStringTypedString = true;139}140141142/**143* Creates a SafeStyle object from a compile-time constant string.144*145* `style` should be in the format146* `name: value; [name: value; ...]` and must not have any < or >147* characters in it. This is so that SafeStyle's contract is preserved,148* allowing the SafeStyle to correctly be interpreted as a sequence of CSS149* declarations and without affecting the syntactic structure of any150* surrounding CSS and HTML.151*152* This method performs basic sanity checks on the format of `style`153* but does not constrain the format of `name` and `value`, except154* for disallowing tag characters.155*156* @param {!Const} style A compile-time-constant string from which157* to create a SafeStyle.158* @return {!SafeStyle} A SafeStyle object initialized to159* `style`.160*/161static fromConstant(style) {162const styleString = Const.unwrap(style);163if (styleString.length === 0) {164return SafeStyle.EMPTY;165}166assert(167endsWith(styleString, ';'),168`Last character of style string is not ';': ${styleString}`);169assert(170contains(styleString, ':'),171'Style string must contain at least one \':\', to ' +172'specify a "name: value" pair: ' + styleString);173return SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(174styleString);175};176177178/**179* Returns this SafeStyle's value as a string.180*181* IMPORTANT: In code where it is security relevant that an object's type is182* indeed `SafeStyle`, use `SafeStyle.unwrap` instead of183* this method. If in doubt, assume that it's security relevant. In184* particular, note that goog.html functions which return a goog.html type do185* not guarantee the returned instance is of the right type. For example:186*187* <pre>188* var fakeSafeHtml = new String('fake');189* fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;190* var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);191* // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by192* // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml193* // instanceof goog.html.SafeHtml.194* </pre>195*196* @return {string}197* @see SafeStyle#unwrap198* @override199*/200getTypedStringValue() {201return this.privateDoNotAccessOrElseSafeStyleWrappedValue_;202}203204205/**206* Returns a string-representation of this value.207*208* To obtain the actual string value wrapped in a SafeStyle, use209* `SafeStyle.unwrap`.210*211* @return {string}212* @see SafeStyle#unwrap213* @override214*/215toString() {216return this.privateDoNotAccessOrElseSafeStyleWrappedValue_.toString();217}218219220/**221* Performs a runtime check that the provided object is indeed a222* SafeStyle object, and returns its value.223*224* @param {!SafeStyle} safeStyle The object to extract from.225* @return {string} The safeStyle object's contained string, unless226* the run-time type check fails. In that case, `unwrap` returns an227* innocuous string, or, if assertions are enabled, throws228* `AssertionError`.229*/230static unwrap(safeStyle) {231// Perform additional Run-time type-checking to ensure that232// safeStyle is indeed an instance of the expected type. This233// provides some additional protection against security bugs due to234// application code that disables type checks.235// Specifically, the following checks are performed:236// 1. The object is an instance of the expected type.237// 2. The object is not an instance of a subclass.238if (safeStyle instanceof SafeStyle && safeStyle.constructor === SafeStyle) {239return safeStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;240} else {241fail(242`expected object of type SafeStyle, got '${safeStyle}` +243'\' of type ' + utils.typeOf(safeStyle));244return 'type_error:SafeStyle';245}246}247248249/**250* Package-internal utility method to create SafeStyle instances.251*252* @param {string} style The string to initialize the SafeStyle object with.253* @return {!SafeStyle} The initialized SafeStyle object.254* @package255*/256static createSafeStyleSecurityPrivateDoNotAccessOrElse(style) {257return new SafeStyle(style, CONSTRUCTOR_TOKEN_PRIVATE);258}259260/**261* Creates a new SafeStyle object from the properties specified in the map.262* @param {!SafeStyle.PropertyMap} map Mapping of property names to263* their values, for example {'margin': '1px'}. Names must consist of264* [-_a-zA-Z0-9]. Values might be strings consisting of265* [-,.'"%_!# a-zA-Z0-9[\]], where ", ', and [] must be properly balanced.266* We also allow simple functions like rgb() and url() which sanitizes its267* contents. Other values must be wrapped in Const. URLs might268* be passed as SafeUrl which will be wrapped into url(""). We269* also support array whose elements are joined with ' '. Null value270* causes skipping the property.271* @return {!SafeStyle}272* @throws {!Error} If invalid name is provided.273* @throws {!AssertionError} If invalid value is provided. With274* disabled assertions, invalid value is replaced by275* SafeStyle.INNOCUOUS_STRING.276*/277static create(map) {278let style = '';279for (let name in map) {280// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty#Using_hasOwnProperty_as_a_property_name281if (Object.prototype.hasOwnProperty.call(map, name)) {282if (!/^[-_a-zA-Z0-9]+$/.test(name)) {283throw new Error(`Name allows only [-_a-zA-Z0-9], got: ${name}`);284}285let value = map[name];286if (value == null) {287continue;288}289if (Array.isArray(value)) {290value = value.map(sanitizePropertyValue).join(' ');291} else {292value = sanitizePropertyValue(value);293}294style += `${name}:${value};`;295}296}297if (!style) {298return SafeStyle.EMPTY;299}300return SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(style);301};302303/**304* Creates a new SafeStyle object by concatenating the values.305* @param {...(!SafeStyle|!Array<!SafeStyle>)} var_args306* SafeStyles to concatenate.307* @return {!SafeStyle}308*/309static concat(var_args) {310let style = '';311312/**313* @param {!SafeStyle|!Array<!SafeStyle>} argument314*/315const addArgument = argument => {316if (Array.isArray(argument)) {317argument.forEach(addArgument);318} else {319style += SafeStyle.unwrap(argument);320}321};322323Array.prototype.forEach.call(arguments, addArgument);324if (!style) {325return SafeStyle.EMPTY;326}327return SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(style);328};329}330331/**332* A SafeStyle instance corresponding to the empty string.333* @const {!SafeStyle}334*/335SafeStyle.EMPTY = SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse('');336337338/**339* The innocuous string generated by SafeStyle.create when passed340* an unsafe value.341* @const {string}342*/343SafeStyle.INNOCUOUS_STRING = 'zClosurez';344345346/**347* A single property value.348* @typedef {string|!Const|!SafeUrl}349*/350SafeStyle.PropertyValue;351352353/**354* Mapping of property names to their values.355* We don't support numbers even though some values might be numbers (e.g.356* line-height or 0 for any length). The reason is that most numeric values need357* units (e.g. '1px') and allowing numbers could cause users forgetting about358* them.359* @typedef {!Object<string, ?SafeStyle.PropertyValue|360* ?Array<!SafeStyle.PropertyValue>>}361*/362SafeStyle.PropertyMap;363364365366/**367* Checks and converts value to string.368* @param {!SafeStyle.PropertyValue} value369* @return {string}370*/371function sanitizePropertyValue(value) {372if (value instanceof SafeUrl) {373const url = SafeUrl.unwrap(value);374return 'url("' + url.replace(/</g, '%3c').replace(/[\\"]/g, '\\$&') + '")';375}376const result = value instanceof Const ?377Const.unwrap(value) :378sanitizePropertyValueString(String(value));379// These characters can be used to change context and we don't want that even380// with const values.381if (/[{;}]/.test(result)) {382throw new AssertionError('Value does not allow [{;}], got: %s.', [result]);383}384return result;385}386387388/**389* Checks string value.390* @param {string} value391* @return {string}392*/393function sanitizePropertyValueString(value) {394// Some CSS property values permit nested functions. We allow one level of395// nesting, and all nested functions must also be in the FUNCTIONS_RE_ list.396const valueWithoutFunctions = value.replace(FUNCTIONS_RE, '$1')397.replace(FUNCTIONS_RE, '$1')398.replace(URL_RE, 'url');399if (!VALUE_RE.test(valueWithoutFunctions)) {400fail(401`String value allows only ${VALUE_ALLOWED_CHARS}` +402' and simple functions, got: ' + value);403return SafeStyle.INNOCUOUS_STRING;404} else if (COMMENT_RE.test(value)) {405fail(`String value disallows comments, got: ${value}`);406return SafeStyle.INNOCUOUS_STRING;407} else if (!hasBalancedQuotes(value)) {408fail(`String value requires balanced quotes, got: ${value}`);409return SafeStyle.INNOCUOUS_STRING;410} else if (!hasBalancedSquareBrackets(value)) {411fail(412'String value requires balanced square brackets and one' +413' identifier per pair of brackets, got: ' + value);414return SafeStyle.INNOCUOUS_STRING;415}416return sanitizeUrl(value);417}418419420/**421* Checks that quotes (" and ') are properly balanced inside a string. Assumes422* that neither escape (\) nor any other character that could result in423* breaking out of a string parsing context are allowed;424* see http://www.w3.org/TR/css3-syntax/#string-token-diagram.425* @param {string} value Untrusted CSS property value.426* @return {boolean} True if property value is safe with respect to quote427* balancedness.428*/429function hasBalancedQuotes(value) {430let outsideSingle = true;431let outsideDouble = true;432for (let i = 0; i < value.length; i++) {433const c = value.charAt(i);434if (c == '\'' && outsideDouble) {435outsideSingle = !outsideSingle;436} else if (c == '"' && outsideSingle) {437outsideDouble = !outsideDouble;438}439}440return outsideSingle && outsideDouble;441}442443444/**445* Checks that square brackets ([ and ]) are properly balanced inside a string,446* and that the content in the square brackets is one ident-token;447* see https://www.w3.org/TR/css-syntax-3/#ident-token-diagram.448* For practicality, and in line with other restrictions posed on SafeStyle449* strings, we restrict the character set allowable in the ident-token to450* [-_a-zA-Z0-9].451* @param {string} value Untrusted CSS property value.452* @return {boolean} True if property value is safe with respect to square453* bracket balancedness.454*/455function hasBalancedSquareBrackets(value) {456let outside = true;457const tokenRe = /^[-_a-zA-Z0-9]$/;458for (let i = 0; i < value.length; i++) {459const c = value.charAt(i);460if (c == ']') {461if (outside) return false; // Unbalanced ].462outside = true;463} else if (c == '[') {464if (!outside) return false; // No nesting.465outside = false;466} else if (!outside && !tokenRe.test(c)) {467return false;468}469}470return outside;471}472473474/**475* Characters allowed in VALUE_RE.476* @type {string}477*/478const VALUE_ALLOWED_CHARS = '[-+,."\'%_!#/ a-zA-Z0-9\\[\\]]';479480481/**482* Regular expression for safe values.483* Quotes (" and ') are allowed, but a check must be done elsewhere to ensure484* they're balanced.485* Square brackets ([ and ]) are allowed, but a check must be done elsewhere486* to ensure they're balanced. The content inside a pair of square brackets must487* be one alphanumeric identifier.488* ',' allows multiple values to be assigned to the same property489* (e.g. background-attachment or font-family) and hence could allow490* multiple values to get injected, but that should pose no risk of XSS.491* The expression checks only for XSS safety, not for CSS validity.492* @const {!RegExp}493*/494const VALUE_RE = new RegExp(`^${VALUE_ALLOWED_CHARS}+\$`);495496497/**498* Regular expression for url(). We support URLs allowed by499* https://www.w3.org/TR/css-syntax-3/#url-token-diagram without using escape500* sequences. Use percent-encoding if you need to use special characters like501* backslash.502* @const {!RegExp}503*/504const URL_RE = new RegExp(505'\\b(url\\([ \t\n]*)(' +506'\'[ -&(-\\[\\]-~]*\'' + // Printable characters except ' and \.507'|"[ !#-\\[\\]-~]*"' + // Printable characters except " and \.508'|[!#-&*-\\[\\]-~]*' + // Printable characters except [ "'()\\].509')([ \t\n]*\\))',510'g');511512/**513* Names of functions allowed in FUNCTIONS_RE.514* @const {!Array<string>}515*/516const ALLOWED_FUNCTIONS = [517'calc',518'cubic-bezier',519'fit-content',520'hsl',521'hsla',522'linear-gradient',523'matrix',524'minmax',525'radial-gradient',526'repeat',527'rgb',528'rgba',529'(rotate|scale|translate)(X|Y|Z|3d)?',530'steps',531'var',532];533534535/**536* Regular expression for simple functions.537* @const {!RegExp}538*/539const FUNCTIONS_RE = new RegExp(540'\\b(' + ALLOWED_FUNCTIONS.join('|') + ')' +541'\\([-+*/0-9a-zA-Z.%#\\[\\], ]+\\)',542'g');543544545/**546* Regular expression for comments. These are disallowed in CSS property values.547* @const {!RegExp}548*/549const COMMENT_RE = /\/\*/;550551552/**553* Sanitize URLs inside url().554* NOTE: We could also consider using CSS.escape once that's available in the555* browsers. However, loosely matching URL e.g. with url\(.*\) and then escaping556* the contents would result in a slightly different language than CSS leading557* to confusion of users. E.g. url(")") is valid in CSS but it would be invalid558* as seen by our parser. On the other hand, url(\) is invalid in CSS but our559* parser would be fine with it.560* @param {string} value Untrusted CSS property value.561* @return {string}562*/563function sanitizeUrl(value) {564return value.replace(URL_RE, (match, before, url, after) => {565let quote = '';566url = url.replace(/^(['"])(.*)\1$/, (match, start, inside) => {567quote = start;568return inside;569});570const sanitized = SafeUrl.sanitize(url).getTypedStringValue();571return before + quote + sanitized + quote + after;572});573}574575576exports = SafeStyle;577578579