| 1 | // Copyright 2014 The Closure Library Authors. All Rights Reserved. |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS-IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | /** |
| 16 | * @fileoverview The SafeStyle type and its builders. |
| 17 | * |
| 18 | * TODO(user): Link to document stating type contract. |
| 19 | */ |
| 20 | |
| 21 | goog.provide('goog.html.SafeStyle'); |
| 22 | |
| 23 | goog.require('goog.array'); |
| 24 | goog.require('goog.asserts'); |
| 25 | goog.require('goog.string'); |
| 26 | goog.require('goog.string.Const'); |
| 27 | goog.require('goog.string.TypedString'); |
| 28 | |
| 29 | |
| 30 | |
| 31 | /** |
| 32 | * A string-like object which represents a sequence of CSS declarations |
| 33 | * ({@code propertyName1: propertyvalue1; propertyName2: propertyValue2; ...}) |
| 34 | * and that carries the security type contract that its value, as a string, |
| 35 | * will not cause untrusted script execution (XSS) when evaluated as CSS in a |
| 36 | * browser. |
| 37 | * |
| 38 | * Instances of this type must be created via the factory methods |
| 39 | * ({@code goog.html.SafeStyle.create} or |
| 40 | * {@code goog.html.SafeStyle.fromConstant}) and not by invoking its |
| 41 | * constructor. The constructor intentionally takes no parameters and the type |
| 42 | * is immutable; hence only a default instance corresponding to the empty string |
| 43 | * can be obtained via constructor invocation. |
| 44 | * |
| 45 | * A SafeStyle's string representation ({@link #getSafeStyleString()}) can |
| 46 | * safely: |
| 47 | * <ul> |
| 48 | * <li>Be interpolated as the entire content of a *quoted* HTML style |
| 49 | * attribute, or before already existing properties. The SafeStyle string |
| 50 | * *must be HTML-attribute-escaped* (where " and ' are escaped) before |
| 51 | * interpolation. |
| 52 | * <li>Be interpolated as the entire content of a {}-wrapped block within a |
| 53 | * stylesheet, or before already existing properties. The SafeStyle string |
| 54 | * should not be escaped before interpolation. SafeStyle's contract also |
| 55 | * guarantees that the string will not be able to introduce new properties |
| 56 | * or elide existing ones. |
| 57 | * <li>Be assigned to the style property of a DOM node. The SafeStyle string |
| 58 | * should not be escaped before being assigned to the property. |
| 59 | * </ul> |
| 60 | * |
| 61 | * A SafeStyle may never contain literal angle brackets. Otherwise, it could |
| 62 | * be unsafe to place a SafeStyle into a <style> tag (where it can't |
| 63 | * be HTML escaped). For example, if the SafeStyle containing |
| 64 | * "{@code font: 'foo <style/><script>evil</script>'}" were |
| 65 | * interpolated within a <style> tag, this would then break out of the |
| 66 | * style context into HTML. |
| 67 | * |
| 68 | * A SafeStyle may contain literal single or double quotes, and as such the |
| 69 | * entire style string must be escaped when used in a style attribute (if |
| 70 | * this were not the case, the string could contain a matching quote that |
| 71 | * would escape from the style attribute). |
| 72 | * |
| 73 | * Values of this type must be composable, i.e. for any two values |
| 74 | * {@code style1} and {@code style2} of this type, |
| 75 | * {@code goog.html.SafeStyle.unwrap(style1) + |
| 76 | * goog.html.SafeStyle.unwrap(style2)} must itself be a value that satisfies |
| 77 | * the SafeStyle type constraint. This requirement implies that for any value |
| 78 | * {@code style} of this type, {@code goog.html.SafeStyle.unwrap(style)} must |
| 79 | * not end in a "property value" or "property name" context. For example, |
| 80 | * a value of {@code background:url("} or {@code font-} would not satisfy the |
| 81 | * SafeStyle contract. This is because concatenating such strings with a |
| 82 | * second value that itself does not contain unsafe CSS can result in an |
| 83 | * overall string that does. For example, if {@code javascript:evil())"} is |
| 84 | * appended to {@code background:url("}, the resulting string may result in |
| 85 | * the execution of a malicious script. |
| 86 | * |
| 87 | * TODO(user): Consider whether we should implement UTF-8 interchange |
| 88 | * validity checks and blacklisting of newlines (including Unicode ones) and |
| 89 | * other whitespace characters (\t, \f). Document here if so and also update |
| 90 | * SafeStyle.fromConstant(). |
| 91 | * |
| 92 | * The following example values comply with this type's contract: |
| 93 | * <ul> |
| 94 | * <li><pre>width: 1em;</pre> |
| 95 | * <li><pre>height:1em;</pre> |
| 96 | * <li><pre>width: 1em;height: 1em;</pre> |
| 97 | * <li><pre>background:url('http://url');</pre> |
| 98 | * </ul> |
| 99 | * In addition, the empty string is safe for use in a CSS attribute. |
| 100 | * |
| 101 | * The following example values do NOT comply with this type's contract: |
| 102 | * <ul> |
| 103 | * <li><pre>background: red</pre> (missing a trailing semi-colon) |
| 104 | * <li><pre>background:</pre> (missing a value and a trailing semi-colon) |
| 105 | * <li><pre>1em</pre> (missing an attribute name, which provides context for |
| 106 | * the value) |
| 107 | * </ul> |
| 108 | * |
| 109 | * @see goog.html.SafeStyle#create |
| 110 | * @see goog.html.SafeStyle#fromConstant |
| 111 | * @see http://www.w3.org/TR/css3-syntax/ |
| 112 | * @constructor |
| 113 | * @final |
| 114 | * @struct |
| 115 | * @implements {goog.string.TypedString} |
| 116 | */ |
| 117 | goog.html.SafeStyle = function() { |
| 118 | /** |
| 119 | * The contained value of this SafeStyle. The field has a purposely |
| 120 | * ugly name to make (non-compiled) code that attempts to directly access this |
| 121 | * field stand out. |
| 122 | * @private {string} |
| 123 | */ |
| 124 | this.privateDoNotAccessOrElseSafeStyleWrappedValue_ = ''; |
| 125 | |
| 126 | /** |
| 127 | * A type marker used to implement additional run-time type checking. |
| 128 | * @see goog.html.SafeStyle#unwrap |
| 129 | * @const |
| 130 | * @private |
| 131 | */ |
| 132 | this.SAFE_STYLE_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = |
| 133 | goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_; |
| 134 | }; |
| 135 | |
| 136 | |
| 137 | /** |
| 138 | * @override |
| 139 | * @const |
| 140 | */ |
| 141 | goog.html.SafeStyle.prototype.implementsGoogStringTypedString = true; |
| 142 | |
| 143 | |
| 144 | /** |
| 145 | * Type marker for the SafeStyle type, used to implement additional |
| 146 | * run-time type checking. |
| 147 | * @const |
| 148 | * @private |
| 149 | */ |
| 150 | goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {}; |
| 151 | |
| 152 | |
| 153 | /** |
| 154 | * Creates a SafeStyle object from a compile-time constant string. |
| 155 | * |
| 156 | * {@code style} should be in the format |
| 157 | * {@code name: value; [name: value; ...]} and must not have any < or > |
| 158 | * characters in it. This is so that SafeStyle's contract is preserved, |
| 159 | * allowing the SafeStyle to correctly be interpreted as a sequence of CSS |
| 160 | * declarations and without affecting the syntactic structure of any |
| 161 | * surrounding CSS and HTML. |
| 162 | * |
| 163 | * This method performs basic sanity checks on the format of {@code style} |
| 164 | * but does not constrain the format of {@code name} and {@code value}, except |
| 165 | * for disallowing tag characters. |
| 166 | * |
| 167 | * @param {!goog.string.Const} style A compile-time-constant string from which |
| 168 | * to create a SafeStyle. |
| 169 | * @return {!goog.html.SafeStyle} A SafeStyle object initialized to |
| 170 | * {@code style}. |
| 171 | */ |
| 172 | goog.html.SafeStyle.fromConstant = function(style) { |
| 173 | var styleString = goog.string.Const.unwrap(style); |
| 174 | if (styleString.length === 0) { |
| 175 | return goog.html.SafeStyle.EMPTY; |
| 176 | } |
| 177 | goog.html.SafeStyle.checkStyle_(styleString); |
| 178 | goog.asserts.assert(goog.string.endsWith(styleString, ';'), |
| 179 | 'Last character of style string is not \';\': ' + styleString); |
| 180 | goog.asserts.assert(goog.string.contains(styleString, ':'), |
| 181 | 'Style string must contain at least one \':\', to ' + |
| 182 | 'specify a "name: value" pair: ' + styleString); |
| 183 | return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse( |
| 184 | styleString); |
| 185 | }; |
| 186 | |
| 187 | |
| 188 | /** |
| 189 | * Checks if the style definition is valid. |
| 190 | * @param {string} style |
| 191 | * @private |
| 192 | */ |
| 193 | goog.html.SafeStyle.checkStyle_ = function(style) { |
| 194 | goog.asserts.assert(!/[<>]/.test(style), |
| 195 | 'Forbidden characters in style string: ' + style); |
| 196 | }; |
| 197 | |
| 198 | |
| 199 | /** |
| 200 | * Returns this SafeStyle's value as a string. |
| 201 | * |
| 202 | * IMPORTANT: In code where it is security relevant that an object's type is |
| 203 | * indeed {@code SafeStyle}, use {@code goog.html.SafeStyle.unwrap} instead of |
| 204 | * this method. If in doubt, assume that it's security relevant. In particular, |
| 205 | * note that goog.html functions which return a goog.html type do not guarantee |
| 206 | * the returned instance is of the right type. For example: |
| 207 | * |
| 208 | * <pre> |
| 209 | * var fakeSafeHtml = new String('fake'); |
| 210 | * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype; |
| 211 | * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml); |
| 212 | * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by |
| 213 | * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml |
| 214 | * // instanceof goog.html.SafeHtml. |
| 215 | * </pre> |
| 216 | * |
| 217 | * @see goog.html.SafeStyle#unwrap |
| 218 | * @override |
| 219 | */ |
| 220 | goog.html.SafeStyle.prototype.getTypedStringValue = function() { |
| 221 | return this.privateDoNotAccessOrElseSafeStyleWrappedValue_; |
| 222 | }; |
| 223 | |
| 224 | |
| 225 | if (goog.DEBUG) { |
| 226 | /** |
| 227 | * Returns a debug string-representation of this value. |
| 228 | * |
| 229 | * To obtain the actual string value wrapped in a SafeStyle, use |
| 230 | * {@code goog.html.SafeStyle.unwrap}. |
| 231 | * |
| 232 | * @see goog.html.SafeStyle#unwrap |
| 233 | * @override |
| 234 | */ |
| 235 | goog.html.SafeStyle.prototype.toString = function() { |
| 236 | return 'SafeStyle{' + |
| 237 | this.privateDoNotAccessOrElseSafeStyleWrappedValue_ + '}'; |
| 238 | }; |
| 239 | } |
| 240 | |
| 241 | |
| 242 | /** |
| 243 | * Performs a runtime check that the provided object is indeed a |
| 244 | * SafeStyle object, and returns its value. |
| 245 | * |
| 246 | * @param {!goog.html.SafeStyle} safeStyle The object to extract from. |
| 247 | * @return {string} The safeStyle object's contained string, unless |
| 248 | * the run-time type check fails. In that case, {@code unwrap} returns an |
| 249 | * innocuous string, or, if assertions are enabled, throws |
| 250 | * {@code goog.asserts.AssertionError}. |
| 251 | */ |
| 252 | goog.html.SafeStyle.unwrap = function(safeStyle) { |
| 253 | // Perform additional Run-time type-checking to ensure that |
| 254 | // safeStyle is indeed an instance of the expected type. This |
| 255 | // provides some additional protection against security bugs due to |
| 256 | // application code that disables type checks. |
| 257 | // Specifically, the following checks are performed: |
| 258 | // 1. The object is an instance of the expected type. |
| 259 | // 2. The object is not an instance of a subclass. |
| 260 | // 3. The object carries a type marker for the expected type. "Faking" an |
| 261 | // object requires a reference to the type marker, which has names intended |
| 262 | // to stand out in code reviews. |
| 263 | if (safeStyle instanceof goog.html.SafeStyle && |
| 264 | safeStyle.constructor === goog.html.SafeStyle && |
| 265 | safeStyle.SAFE_STYLE_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ === |
| 266 | goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) { |
| 267 | return safeStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_; |
| 268 | } else { |
| 269 | goog.asserts.fail( |
| 270 | 'expected object of type SafeStyle, got \'' + safeStyle + '\''); |
| 271 | return 'type_error:SafeStyle'; |
| 272 | } |
| 273 | }; |
| 274 | |
| 275 | |
| 276 | /** |
| 277 | * Package-internal utility method to create SafeStyle instances. |
| 278 | * |
| 279 | * @param {string} style The string to initialize the SafeStyle object with. |
| 280 | * @return {!goog.html.SafeStyle} The initialized SafeStyle object. |
| 281 | * @package |
| 282 | */ |
| 283 | goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse = |
| 284 | function(style) { |
| 285 | return new goog.html.SafeStyle().initSecurityPrivateDoNotAccessOrElse_(style); |
| 286 | }; |
| 287 | |
| 288 | |
| 289 | /** |
| 290 | * Called from createSafeStyleSecurityPrivateDoNotAccessOrElse(). This |
| 291 | * method exists only so that the compiler can dead code eliminate static |
| 292 | * fields (like EMPTY) when they're not accessed. |
| 293 | * @param {string} style |
| 294 | * @return {!goog.html.SafeStyle} |
| 295 | * @private |
| 296 | */ |
| 297 | goog.html.SafeStyle.prototype.initSecurityPrivateDoNotAccessOrElse_ = function( |
| 298 | style) { |
| 299 | this.privateDoNotAccessOrElseSafeStyleWrappedValue_ = style; |
| 300 | return this; |
| 301 | }; |
| 302 | |
| 303 | |
| 304 | /** |
| 305 | * A SafeStyle instance corresponding to the empty string. |
| 306 | * @const {!goog.html.SafeStyle} |
| 307 | */ |
| 308 | goog.html.SafeStyle.EMPTY = |
| 309 | goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(''); |
| 310 | |
| 311 | |
| 312 | /** |
| 313 | * The innocuous string generated by goog.html.SafeUrl.create when passed |
| 314 | * an unsafe value. |
| 315 | * @const {string} |
| 316 | */ |
| 317 | goog.html.SafeStyle.INNOCUOUS_STRING = 'zClosurez'; |
| 318 | |
| 319 | |
| 320 | /** |
| 321 | * Mapping of property names to their values. |
| 322 | * @typedef {!Object<string, goog.string.Const|string>} |
| 323 | */ |
| 324 | goog.html.SafeStyle.PropertyMap; |
| 325 | |
| 326 | |
| 327 | /** |
| 328 | * Creates a new SafeStyle object from the properties specified in the map. |
| 329 | * @param {goog.html.SafeStyle.PropertyMap} map Mapping of property names to |
| 330 | * their values, for example {'margin': '1px'}. Names must consist of |
| 331 | * [-_a-zA-Z0-9]. Values might be strings consisting of |
| 332 | * [-,.'"%_!# a-zA-Z0-9], where " and ' must be properly balanced. |
| 333 | * Other values must be wrapped in goog.string.Const. Null value causes |
| 334 | * skipping the property. |
| 335 | * @return {!goog.html.SafeStyle} |
| 336 | * @throws {Error} If invalid name is provided. |
| 337 | * @throws {goog.asserts.AssertionError} If invalid value is provided. With |
| 338 | * disabled assertions, invalid value is replaced by |
| 339 | * goog.html.SafeStyle.INNOCUOUS_STRING. |
| 340 | */ |
| 341 | goog.html.SafeStyle.create = function(map) { |
| 342 | var style = ''; |
| 343 | for (var name in map) { |
| 344 | if (!/^[-_a-zA-Z0-9]+$/.test(name)) { |
| 345 | throw Error('Name allows only [-_a-zA-Z0-9], got: ' + name); |
| 346 | } |
| 347 | var value = map[name]; |
| 348 | if (value == null) { |
| 349 | continue; |
| 350 | } |
| 351 | if (value instanceof goog.string.Const) { |
| 352 | value = goog.string.Const.unwrap(value); |
| 353 | // These characters can be used to change context and we don't want that |
| 354 | // even with const values. |
| 355 | goog.asserts.assert(!/[{;}]/.test(value), 'Value does not allow [{;}].'); |
| 356 | } else if (!goog.html.SafeStyle.VALUE_RE_.test(value)) { |
| 357 | goog.asserts.fail( |
| 358 | 'String value allows only [-,."\'%_!# a-zA-Z0-9], got: ' + value); |
| 359 | value = goog.html.SafeStyle.INNOCUOUS_STRING; |
| 360 | } else if (!goog.html.SafeStyle.hasBalancedQuotes_(value)) { |
| 361 | goog.asserts.fail('String value requires balanced quotes, got: ' + value); |
| 362 | value = goog.html.SafeStyle.INNOCUOUS_STRING; |
| 363 | } |
| 364 | style += name + ':' + value + ';'; |
| 365 | } |
| 366 | if (!style) { |
| 367 | return goog.html.SafeStyle.EMPTY; |
| 368 | } |
| 369 | goog.html.SafeStyle.checkStyle_(style); |
| 370 | return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse( |
| 371 | style); |
| 372 | }; |
| 373 | |
| 374 | |
| 375 | /** |
| 376 | * Checks that quotes (" and ') are properly balanced inside a string. Assumes |
| 377 | * that neither escape (\) nor any other character that could result in |
| 378 | * breaking out of a string parsing context are allowed; |
| 379 | * see http://www.w3.org/TR/css3-syntax/#string-token-diagram. |
| 380 | * @param {string} value Untrusted CSS property value. |
| 381 | * @return {boolean} True if property value is safe with respect to quote |
| 382 | * balancedness. |
| 383 | * @private |
| 384 | */ |
| 385 | goog.html.SafeStyle.hasBalancedQuotes_ = function(value) { |
| 386 | var outsideSingle = true; |
| 387 | var outsideDouble = true; |
| 388 | for (var i = 0; i < value.length; i++) { |
| 389 | var c = value.charAt(i); |
| 390 | if (c == "'" && outsideDouble) { |
| 391 | outsideSingle = !outsideSingle; |
| 392 | } else if (c == '"' && outsideSingle) { |
| 393 | outsideDouble = !outsideDouble; |
| 394 | } |
| 395 | } |
| 396 | return outsideSingle && outsideDouble; |
| 397 | }; |
| 398 | |
| 399 | |
| 400 | // Keep in sync with the error string in create(). |
| 401 | /** |
| 402 | * Regular expression for safe values. |
| 403 | * |
| 404 | * Quotes (" and ') are allowed, but a check must be done elsewhere to ensure |
| 405 | * they're balanced. |
| 406 | * |
| 407 | * ',' allows multiple values to be assigned to the same property |
| 408 | * (e.g. background-attachment or font-family) and hence could allow |
| 409 | * multiple values to get injected, but that should pose no risk of XSS. |
| 410 | * @const {!RegExp} |
| 411 | * @private |
| 412 | */ |
| 413 | goog.html.SafeStyle.VALUE_RE_ = /^[-,."'%_!# a-zA-Z0-9]+$/; |
| 414 | |
| 415 | |
| 416 | /** |
| 417 | * Creates a new SafeStyle object by concatenating the values. |
| 418 | * @param {...(!goog.html.SafeStyle|!Array<!goog.html.SafeStyle>)} var_args |
| 419 | * SafeStyles to concatenate. |
| 420 | * @return {!goog.html.SafeStyle} |
| 421 | */ |
| 422 | goog.html.SafeStyle.concat = function(var_args) { |
| 423 | var style = ''; |
| 424 | |
| 425 | /** |
| 426 | * @param {!goog.html.SafeStyle|!Array<!goog.html.SafeStyle>} argument |
| 427 | */ |
| 428 | var addArgument = function(argument) { |
| 429 | if (goog.isArray(argument)) { |
| 430 | goog.array.forEach(argument, addArgument); |
| 431 | } else { |
| 432 | style += goog.html.SafeStyle.unwrap(argument); |
| 433 | } |
| 434 | }; |
| 435 | |
| 436 | goog.array.forEach(arguments, addArgument); |
| 437 | if (!style) { |
| 438 | return goog.html.SafeStyle.EMPTY; |
| 439 | } |
| 440 | return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse( |
| 441 | style); |
| 442 | }; |