Path: blob/trunk/third_party/closure/goog/html/safestylesheet.js
4560 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview The SafeStyleSheet type and its builders.8*9* TODO(xtof): Link to document stating type contract.10*/1112goog.module('goog.html.SafeStyleSheet');13goog.module.declareLegacyNamespace();1415const Const = goog.require('goog.string.Const');16const SafeStyle = goog.require('goog.html.SafeStyle');17const TypedString = goog.require('goog.string.TypedString');18const googObject = goog.require('goog.object');19const {assert, fail} = goog.require('goog.asserts');20const {contains} = goog.require('goog.string.internal');21const utils = goog.require('goog.utils');2223/**24* Token used to ensure that object is created only from this file. No code25* outside of this file can access this token.26* @const {!Object}27*/28const CONSTRUCTOR_TOKEN_PRIVATE = {};2930/**31* A string-like object which represents a CSS style sheet and that carries the32* security type contract that its value, as a string, will not cause untrusted33* script execution (XSS) when evaluated as CSS in a browser.34*35* Instances of this type must be created via the factory method36* `SafeStyleSheet.fromConstant` and not by invoking its constructor. The37* constructor intentionally takes an extra parameter that cannot be constructed38* outside of this file and the type is immutable; hence only a default instance39* corresponding to the empty string can be obtained via constructor invocation.40*41* A SafeStyleSheet's string representation can safely be interpolated as the42* content of a style element within HTML. The SafeStyleSheet string should43* not be escaped before interpolation.44*45* Values of this type must be composable, i.e. for any two values46* `styleSheet1` and `styleSheet2` of this type,47* `SafeStyleSheet.unwrap(styleSheet1) + SafeStyleSheet.unwrap(styleSheet2)`48* must itself be a value that satisfies the SafeStyleSheet type constraint.49* This requirement implies that for any value `styleSheet` of this type,50* `SafeStyleSheet.unwrap(styleSheet1)` must end in51* "beginning of rule" context.52*53* A SafeStyleSheet can be constructed via security-reviewed unchecked54* conversions. In this case producers of SafeStyleSheet must ensure themselves55* that the SafeStyleSheet does not contain unsafe script. Note in particular56* that `<` is dangerous, even when inside CSS strings, and so should57* always be forbidden or CSS-escaped in user controlled input. For example, if58* `</style><script>evil</script>"` were interpolated59* inside a CSS string, it would break out of the context of the original60* style element and `evil` would execute. Also note that within an HTML61* style (raw text) element, HTML character references, such as62* `&lt;`, are not allowed. See63* http://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements64* (similar considerations apply to the style element).65*66* @see SafeStyleSheet#fromConstant67* @final68* @implements {TypedString}69*/70class SafeStyleSheet {71/**72* @param {string} value73* @param {!Object} token package-internal implementation detail.74*/75constructor(value, token) {76if (goog.DEBUG && token !== CONSTRUCTOR_TOKEN_PRIVATE) {77throw Error('SafeStyleSheet is not meant to be built directly');78}7980/**81* The contained value of this SafeStyleSheet. The field has a purposely82* ugly name to make (non-compiled) code that attempts to directly access83* this field stand out.84* @const85* @private {string}86*/87this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_ = value;8889/**90* @override91* @const92*/93this.implementsGoogStringTypedString = true;94}9596/**97* Returns a string-representation of this value.98*99* To obtain the actual string value wrapped in a SafeStyleSheet, use100* `SafeStyleSheet.unwrap`.101*102* @return {string}103* @see SafeStyleSheet#unwrap104* @override105*/106toString() {107return this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_.toString();108}109110/**111* Creates a style sheet consisting of one selector and one style definition.112* Use {@link SafeStyleSheet.concat} to create longer style sheets.113* This function doesn't support @import, @media and similar constructs.114* @param {string} selector CSS selector, e.g. '#id' or 'tag .class, #id'. We115* support CSS3 selectors: https://w3.org/TR/css3-selectors/#selectors.116* @param {!SafeStyle.PropertyMap|!SafeStyle} style Style117* definition associated with the selector.118* @return {!SafeStyleSheet}119* @throws {!Error} If invalid selector is provided.120*/121static createRule(selector, style) {122if (contains(selector, '<')) {123throw new Error(`Selector does not allow '<', got: ${selector}`);124}125126// Remove strings.127const selectorToCheck =128selector.replace(/('|")((?!\1)[^\r\n\f\\]|\\[\s\S])*\1/g, '');129130// Check characters allowed in CSS3 selectors.131if (!/^[-_a-zA-Z0-9#.:* ,>+~[\]()=\\^$|]+$/.test(selectorToCheck)) {132throw new Error(133'Selector allows only [-_a-zA-Z0-9#.:* ,>+~[\\]()=\\^$|] and ' +134'strings, got: ' + selector);135}136137// Check balanced () and [].138if (!SafeStyleSheet.hasBalancedBrackets_(selectorToCheck)) {139throw new Error(140'() and [] in selector must be balanced, got: ' + selector);141}142143if (!(style instanceof SafeStyle)) {144style = SafeStyle.create(style);145}146const styleSheet =147`${selector}{` + SafeStyle.unwrap(style).replace(/</g, '\\3C ') + '}';148return SafeStyleSheet.createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(149styleSheet);150}151152/**153* Checks if a string has balanced () and [] brackets.154* @param {string} s String to check.155* @return {boolean}156* @private157*/158static hasBalancedBrackets_(s) {159const brackets = {'(': ')', '[': ']'};160const expectedBrackets = [];161for (let i = 0; i < s.length; i++) {162const ch = s[i];163if (brackets[ch]) {164expectedBrackets.push(brackets[ch]);165} else if (googObject.contains(brackets, ch)) {166if (expectedBrackets.pop() != ch) {167return false;168}169}170}171return expectedBrackets.length == 0;172}173174/**175* Creates a new SafeStyleSheet object by concatenating values.176* @param {...(!SafeStyleSheet|!Array<!SafeStyleSheet>)}177* var_args Values to concatenate.178* @return {!SafeStyleSheet}179*/180static concat(var_args) {181let result = '';182183/**184* @param {!SafeStyleSheet|!Array<!SafeStyleSheet>}185* argument186*/187const addArgument = argument => {188if (Array.isArray(argument)) {189argument.forEach(addArgument);190} else {191result += SafeStyleSheet.unwrap(argument);192}193};194195Array.prototype.forEach.call(arguments, addArgument);196return SafeStyleSheet.createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(197result);198}199200/**201* Creates a SafeStyleSheet object from a compile-time constant string.202*203* `styleSheet` must not have any < characters in it, so that204* the syntactic structure of the surrounding HTML is not affected.205*206* @param {!Const} styleSheet A compile-time-constant string from207* which to create a SafeStyleSheet.208* @return {!SafeStyleSheet} A SafeStyleSheet object initialized to209* `styleSheet`.210*/211static fromConstant(styleSheet) {212const styleSheetString = Const.unwrap(styleSheet);213if (styleSheetString.length === 0) {214return SafeStyleSheet.EMPTY;215}216// > is a valid character in CSS selectors and there's no strict need to217// block it if we already block <.218assert(219!contains(styleSheetString, '<'),220`Forbidden '<' character in style sheet string: ${styleSheetString}`);221return SafeStyleSheet.createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(222styleSheetString);223}224225/**226* Returns this SafeStyleSheet's value as a string.227*228* IMPORTANT: In code where it is security relevant that an object's type is229* indeed `SafeStyleSheet`, use `SafeStyleSheet.unwrap`230* instead of this method. If in doubt, assume that it's security relevant. In231* particular, note that goog.html functions which return a goog.html type do232* not guarantee the returned instance is of the right type. For example:233*234* <pre>235* var fakeSafeHtml = new String('fake');236* fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;237* var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);238* // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by239* // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml240* // instanceof goog.html.SafeHtml.241* </pre>242*243* @see SafeStyleSheet#unwrap244* @override245*/246getTypedStringValue() {247return this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_;248}249250/**251* Performs a runtime check that the provided object is indeed a252* SafeStyleSheet object, and returns its value.253*254* @param {!SafeStyleSheet} safeStyleSheet The object to extract from.255* @return {string} The safeStyleSheet object's contained string, unless256* the run-time type check fails. In that case, `unwrap` returns an257* innocuous string, or, if assertions are enabled, throws258* `asserts.AssertionError`.259*/260static unwrap(safeStyleSheet) {261// Perform additional Run-time type-checking to ensure that262// safeStyleSheet is indeed an instance of the expected type. This263// provides some additional protection against security bugs due to264// application code that disables type checks.265// Specifically, the following checks are performed:266// 1. The object is an instance of the expected type.267// 2. The object is not an instance of a subclass.268if (safeStyleSheet instanceof SafeStyleSheet &&269safeStyleSheet.constructor === SafeStyleSheet) {270return safeStyleSheet.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_;271} else {272fail(273'expected object of type SafeStyleSheet, got \'' + safeStyleSheet +274'\' of type ' + utils.typeOf(safeStyleSheet));275return 'type_error:SafeStyleSheet';276}277}278279/**280* Package-internal utility method to create SafeStyleSheet instances.281*282* @param {string} styleSheet The string to initialize the SafeStyleSheet283* object with.284* @return {!SafeStyleSheet} The initialized SafeStyleSheet object.285* @package286*/287static createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(styleSheet) {288return new SafeStyleSheet(styleSheet, CONSTRUCTOR_TOKEN_PRIVATE);289}290}291292/**293* A SafeStyleSheet instance corresponding to the empty string.294* @const {!SafeStyleSheet}295*/296SafeStyleSheet.EMPTY =297SafeStyleSheet.createSafeStyleSheetSecurityPrivateDoNotAccessOrElse('');298299300exports = SafeStyleSheet;301302303