001 // Copyright (c) 2011, Mike Samuel 002 // All rights reserved. 003 // 004 // Redistribution and use in source and binary forms, with or without 005 // modification, are permitted provided that the following conditions 006 // are met: 007 // 008 // Redistributions of source code must retain the above copyright 009 // notice, this list of conditions and the following disclaimer. 010 // Redistributions in binary form must reproduce the above copyright 011 // notice, this list of conditions and the following disclaimer in the 012 // documentation and/or other materials provided with the distribution. 013 // Neither the name of the OWASP nor the names of its contributors may 014 // be used to endorse or promote products derived from this software 015 // without specific prior written permission. 016 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 017 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 018 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 019 // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 020 // COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 021 // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 022 // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 023 // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 024 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 025 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 026 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 027 // POSSIBILITY OF SUCH DAMAGE. 028 029 package org.owasp.html; 030 031 import java.util.List; 032 import java.util.Map; 033 import java.util.Set; 034 import java.util.regex.Pattern; 035 036 import javax.annotation.Nullable; 037 import javax.annotation.concurrent.NotThreadSafe; 038 039 import com.google.common.base.Function; 040 import com.google.common.base.Predicate; 041 import com.google.common.collect.ImmutableList; 042 import com.google.common.collect.ImmutableMap; 043 import com.google.common.collect.ImmutableSet; 044 import com.google.common.collect.Maps; 045 import com.google.common.collect.Sets; 046 047 048 /** 049 * Conveniences for configuring policies for the {@link HtmlSanitizer}. 050 * 051 * <h3>Usage</h3> 052 * <p> 053 * To create a policy, first construct an instance of this class; then call 054 * <code>allow…</code> methods to turn on tags, attributes, and other 055 * processing modes; and finally call <code>build()</code> or 056 * <code>toFactory()</code>. 057 * </p> 058 * <pre class="prettyprint lang-java"> 059 * // Define the policy. 060 * Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> policyDefinition 061 * = new HtmlPolicyBuilder() 062 * .allowElements("a", "p") 063 * .allowAttributes("href").onElements("a") 064 * .toFactory(); 065 * 066 * // Sanitize your output. 067 * HtmlSanitizer.sanitize(myHtml. policyDefinition.apply(myHtmlStreamRenderer)); 068 * </pre> 069 * 070 * <h3>Embedded Content</h3> 071 * <p> 072 * Embedded URLs are filtered by 073 * {@link HtmlPolicyBuilder#allowUrlProtocols protocol}. 074 * There is a {@link HtmlPolicyBuilder#allowStandardUrlProtocols canned policy} 075 * so you can easily white-list widely used policies that don't violate the 076 * current pages origin. See "Customization" below for ways to do further 077 * filtering. If you allow links it might be worthwhile to 078 * {@link HtmlPolicyBuilder#requireRelNofollowOnLinks() require} 079 * {@code rel=nofollow}. 080 * </p> 081 * <p> 082 * This class simply throws out all embedded JS. 083 * Use a custom element or attribute policy to allow through 084 * signed or otherwise known-safe code. 085 * Check out the Caja project if you need a way to contain third-party JS. 086 * </p> 087 * <p> 088 * This class does not attempt to faithfully parse and sanitize CSS. 089 * It does provide {@link HtmlPolicyBuilder#allowStyling() one} styling option 090 * that allows through a few CSS properties that allow textual styling, but that 091 * disallow image loading, history stealing, layout breaking, code execution, 092 * etc. 093 * </p> 094 * 095 * <h3>Customization</h3> 096 * <p> 097 * You can easily do custom processing on tags and attributes by supplying your 098 * own {@link ElementPolicy element policy} or 099 * {@link AttributePolicy attribute policy} when calling 100 * <code>allow…</code>. 101 * E.g. to convert headers into {@code <div>}s, you could use an element policy 102 * </p> 103 * <pre class="prettyprint lang-java"> 104 * new HtmlPolicyBuilder 105 * .allowElement( 106 * new ElementPolicy() { 107 * public String apply(String elementName, List<String> attributes) { 108 * attributes.add("class"); 109 * attributes.add("header-" + elementName); 110 * return "div"; 111 * } 112 * }, 113 * "h1", "h2", "h3", "h4", "h5", "h6") 114 * .build(outputChannel) 115 * </pre> 116 * 117 * <h3>Rules of Thumb</h3> 118 * <p> 119 * Throughout this class, several rules hold: 120 * <ul> 121 * <li>Everything is denied by default. There are 122 * <code>disallow…</code> methods, but those reverse 123 * allows instead of rolling back overly permissive defaults. 124 * <li>The order of allows and disallows does not matter. 125 * Disallows trump allows whether they occur before or after them. 126 * The only method that needs to be called in a particular place is 127 * {@link HtmlPolicyBuilder#build}. 128 * Allows or disallows after {@code build} is called have no 129 * effect on the already built policy. 130 * <li>Element and attribute policies are applied in the following order: 131 * element specific attribute policy, global attribute policy, element 132 * policy. 133 * Element policies come last so they can observe all the post-processed 134 * attributes, and so they can add attributes that are exempt from 135 * attribute policies. 136 * Element specific policies go first, so they can normalize content to 137 * a form that might be acceptable to a more simplistic global policy. 138 * </ul> 139 * 140 * <h3>Thread safety and efficiency</h3> 141 * <p> 142 * This class is not thread-safe. The resulting policy will not violate its 143 * security guarantees as a result of race conditions, but is not thread safe 144 * because it maintains state to track whether text inside disallowed elements 145 * should be suppressed. 146 * <p> 147 * The resulting policy can be reused, but if you use the 148 * {@link HtmlPolicyBuilder#toFactory()} method instead of {@link #build}, then 149 * binding policies to output channels is cheap so there's no need. 150 * </p> 151 * 152 * @author Mike Samuel <mikesamuel@gmail.com> 153 */ 154 @TCB 155 @NotThreadSafe 156 public class HtmlPolicyBuilder { 157 /** 158 * The default set of elements that are removed if they have no attributes. 159 * Since {@code <img>} is in this set, by default, a policy will remove 160 * {@code <img src=javascript:alert(1337)>} because its URL is not allowed 161 * and it has no other attributes that would warrant it appearing in the 162 * output. 163 */ 164 public static final ImmutableSet<String> DEFAULT_SKIP_IF_EMPTY 165 = ImmutableSet.of("a", "font", "img", "input", "span"); 166 167 private final Map<String, ElementPolicy> elPolicies = Maps.newLinkedHashMap(); 168 private final Map<String, Map<String, AttributePolicy>> attrPolicies 169 = Maps.newLinkedHashMap(); 170 private final Map<String, AttributePolicy> globalAttrPolicies 171 = Maps.newLinkedHashMap(); 172 private final Set<String> allowedProtocols = Sets.newLinkedHashSet(); 173 private final Set<String> skipIfEmpty = Sets.newLinkedHashSet( 174 DEFAULT_SKIP_IF_EMPTY); 175 private boolean requireRelNofollowOnLinks, allowStyling; 176 177 /** 178 * Allows the named elements. 179 */ 180 public HtmlPolicyBuilder allowElements(String... elementNames) { 181 return allowElements(ElementPolicy.IDENTITY_ELEMENT_POLICY, elementNames); 182 } 183 184 /** 185 * Disallows the named elements. Elements are disallowed by default, so 186 * there is no need to disallow elements, unless you are making an exception 187 * based on an earlier allow. 188 */ 189 public HtmlPolicyBuilder disallowElements(String... elementNames) { 190 return allowElements(ElementPolicy.REJECT_ALL_ELEMENT_POLICY, elementNames); 191 } 192 193 /** 194 * Allow the given elements with the given policy. 195 * 196 * @param policy May remove or add attributes, change the element name, or 197 * deny the element. 198 */ 199 public HtmlPolicyBuilder allowElements( 200 ElementPolicy policy, String... elementNames) { 201 invalidateCompiledState(); 202 for (String elementName : elementNames) { 203 elementName = HtmlLexer.canonicalName(elementName); 204 ElementPolicy newPolicy = ElementPolicy.Util.join( 205 elPolicies.get(elementName), policy); 206 // Don't remove if newPolicy is the always reject policy since we want 207 // that to infect later allowElement calls for this particular element 208 // name. rejects should have higher priority than allows. 209 elPolicies.put(elementName, newPolicy); 210 } 211 return this; 212 } 213 214 /** 215 * A canned policy that allows a number of common formatting elements. 216 */ 217 public HtmlPolicyBuilder allowCommonInlineFormattingElements() { 218 return allowElements( 219 "b", "i", "font", "s", "u", "o", "sup", "sub", "ins", "del", "strong", 220 "strike", "tt", "code", "big", "small", "br", "span"); 221 } 222 223 /** 224 * A canned policy that allows a number of common block elements. 225 */ 226 public HtmlPolicyBuilder allowCommonBlockElements() { 227 return allowElements( 228 "p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", 229 "blockquote"); 230 } 231 232 /** 233 * Assuming the given elements are allowed, allows them to appear without 234 * attributes. 235 * 236 * @see #DEFAULT_SKIP_IF_EMPTY 237 * @see #disallowWithoutAttributes 238 */ 239 public HtmlPolicyBuilder allowWithoutAttributes(String... elementNames) { 240 invalidateCompiledState(); 241 for (String elementName : elementNames) { 242 elementName = HtmlLexer.canonicalName(elementName); 243 skipIfEmpty.remove(elementName); 244 } 245 return this; 246 } 247 248 /** 249 * Disallows the given elements from appearing without attributes. 250 * 251 * @see #DEFAULT_SKIP_IF_EMPTY 252 * @see #allowWithoutAttributes 253 */ 254 public HtmlPolicyBuilder disallowWithoutAttributes(String... elementNames) { 255 invalidateCompiledState(); 256 for (String elementName : elementNames) { 257 elementName = HtmlLexer.canonicalName(elementName); 258 skipIfEmpty.add(elementName); 259 } 260 return this; 261 } 262 263 /** 264 * Returns an object that lets you associate policies with the given 265 * attributes, and allow them globally or on specific elements. 266 */ 267 public AttributeBuilder allowAttributes(String... attributeNames) { 268 ImmutableList.Builder<String> b = ImmutableList.builder(); 269 for (String attributeName : attributeNames) { 270 b.add(HtmlLexer.canonicalName(attributeName)); 271 } 272 return new AttributeBuilder(b.build()); 273 } 274 275 /** 276 * Reverse an earlier attribute {@link #allowAttributes allow}. 277 * <p> 278 * For this to have an effect you must call at least one of 279 * {@link AttributeBuilder#globally} and {@link AttributeBuilder#onElements}. 280 * <p> 281 * Attributes are disallowed by default, so there is no need to call this 282 * with a laundry list of attribute/element pairs. 283 */ 284 public AttributeBuilder disallowAttributes(String... attributeNames) { 285 return this.allowAttributes(attributeNames) 286 .matching(AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY); 287 } 288 289 290 private HtmlPolicyBuilder allowAttributesGlobally( 291 AttributePolicy policy, List<String> attributeNames) { 292 invalidateCompiledState(); 293 for (String attributeName : attributeNames) { 294 // We reinterpret the identity policy later via policy joining since its 295 // the default passed from the policy-less method, but we don't do 296 // anything here since we don't know until build() is called whether the 297 // policy author wants to allow certain URL protocols or wants to deal 298 // with styles. 299 AttributePolicy oldPolicy = globalAttrPolicies.get(attributeName); 300 globalAttrPolicies.put( 301 attributeName, AttributePolicy.Util.join(oldPolicy, policy)); 302 } 303 return this; 304 } 305 306 private HtmlPolicyBuilder allowAttributesOnElements( 307 AttributePolicy policy, List<String> attributeNames, 308 List<String> elementNames) { 309 invalidateCompiledState(); 310 for (String elementName : elementNames) { 311 Map<String, AttributePolicy> policies = attrPolicies.get(elementName); 312 if (policies == null) { 313 policies = Maps.newLinkedHashMap(); 314 attrPolicies.put(elementName, policies); 315 } 316 for (String attributeName : attributeNames) { 317 AttributePolicy oldPolicy = policies.get(attributeName); 318 policies.put( 319 attributeName, 320 AttributePolicy.Util.join(oldPolicy, policy)); 321 } 322 } 323 return this; 324 } 325 326 /** 327 * Adds <a href="http://en.wikipedia.org/wiki/Nofollow"><code>rel=nofollow</code></a> 328 * to links. 329 */ 330 public HtmlPolicyBuilder requireRelNofollowOnLinks() { 331 invalidateCompiledState(); 332 this.requireRelNofollowOnLinks = true; 333 return this; 334 } 335 336 /** 337 * Adds to the set of protocols that are allowed in URL attributes. 338 * For each URL attribute that is allowed, we further constrain it by 339 * only allowing the value through if it specifies no protocol, or if it 340 * specifies one in the allowedProtocols white-list. 341 * This is done regardless of whether any protocols have been allowed, so 342 * allowing the attribute "href" globally with the identity policy but 343 * not white-listing any protocols, effectively disallows the "href" 344 * attribute globally. 345 * <p> 346 * Do not allow any <code>*script</code> such as <code>javascript</code> 347 * protocols if you might use this policy with untrusted code. 348 */ 349 public HtmlPolicyBuilder allowUrlProtocols(String... protocols) { 350 invalidateCompiledState(); 351 // If there is at least one allowed protocol, then allow URLs and 352 // add a filter that checks href and src values. 353 354 // Do not allow href and srcs through otherwise, and only allow on images 355 // and links. 356 for (String protocol : protocols) { 357 protocol = Strings.toLowerCase(protocol); 358 allowedProtocols.add(protocol); 359 } 360 return this; 361 } 362 363 /** 364 * Reverses a decision made by {@link #allowUrlProtocols}. 365 */ 366 public HtmlPolicyBuilder disallowUrlProtocols(String... protocols) { 367 invalidateCompiledState(); 368 for (String protocol : protocols) { 369 protocol = Strings.toLowerCase(protocol); 370 allowedProtocols.remove(protocol); 371 } 372 return this; 373 } 374 375 /** 376 * A canned URL protocol policy that allows <code>http</code>, 377 * <code>https</code>, and <code>mailto</code>. 378 */ 379 public HtmlPolicyBuilder allowStandardUrlProtocols() { 380 return allowUrlProtocols("http", "https", "mailto"); 381 } 382 383 /** 384 * Convert <code>style="<CSS>"</code> to simple non-JS containing 385 * <code><font></code> tags to allow color, font-size, typeface, and 386 * other styling. 387 */ 388 public HtmlPolicyBuilder allowStyling() { 389 invalidateCompiledState(); 390 allowStyling = true; 391 return this; 392 } 393 394 /** 395 * Names of attributes from HTML 4 whose values are URLs. 396 * Other attributes, e.g. <code>style</code> may contain URLs even though 397 * there values are not URLs. 398 */ 399 private static final Set<String> URL_ATTRIBUTE_NAMES = ImmutableSet.of( 400 "action", "archive", "background", "cite", "classid", "codebase", "data", 401 "dsync", "formaction", "href", "icon", "longdesc", "manifest", "poster", 402 "profile", "src", "usemap"); 403 404 /** 405 * Produces a policy based on the allow and disallow calls previously made. 406 * 407 * @param out receives calls to open only tags allowed by 408 * previous calls to this object. 409 * Typically a {@link HtmlStreamRenderer}. 410 */ 411 public HtmlSanitizer.Policy build(HtmlStreamEventReceiver out) { 412 return toFactory().apply(out); 413 } 414 415 /** 416 * Like {@link #build} but can be reused to create many different policies 417 * each backed by a different output channel. 418 */ 419 public Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> toFactory() { 420 return new Factory(compilePolicies(), allowStyling); 421 } 422 423 // Speed up subsequent builds by caching the compiled policies. 424 private transient ImmutableMap<String, ElementAndAttributePolicies> 425 compiledPolicies; 426 427 /** Called by mutators to signal that any compiled policy is out-of-date. */ 428 private void invalidateCompiledState() { 429 compiledPolicies = null; 430 } 431 432 private ImmutableMap<String, ElementAndAttributePolicies> compilePolicies() { 433 if (compiledPolicies != null) { return compiledPolicies; } 434 435 // Copy maps before normalizing in case builder is reused. 436 Map<String, ElementPolicy> elPolicies 437 = Maps.newLinkedHashMap(this.elPolicies); 438 Map<String, Map<String, AttributePolicy>> attrPolicies 439 = Maps.newLinkedHashMap(this.attrPolicies); 440 for (Map.Entry<String, Map<String, AttributePolicy>> e : 441 attrPolicies.entrySet()) { 442 e.setValue(Maps.newLinkedHashMap(e.getValue())); 443 } 444 Map<String, AttributePolicy> globalAttrPolicies 445 = Maps.newLinkedHashMap(this.globalAttrPolicies); 446 Set<String> allowedProtocols = ImmutableSet.copyOf(this.allowedProtocols); 447 448 // Implement requireRelNofollowOnLinks 449 if (requireRelNofollowOnLinks) { 450 elPolicies.put( 451 "a", 452 ElementPolicy.Util.join( 453 elPolicies.get("a"), 454 new ElementPolicy() { 455 public String apply(String elementName, List<String> attrs) { 456 for (int i = 0, n = attrs.size(); i < n; i += 2) { 457 if ("href".equals(attrs.get(i))) { 458 attrs.add("rel"); 459 attrs.add("nofollow"); 460 break; 461 } 462 } 463 return elementName; 464 } 465 })); 466 } 467 468 // Implement protocol policies. 469 // For each URL attribute that is allowed, we further constrain it by 470 // only allowing the value through if it specifies no protocol, or if it 471 // specifies one in the allowedProtocols white-list. 472 // This is done regardless of whether any protocols have been allowed, so 473 // allowing the attribute "href" globally with the identity policy but 474 // not white-listing any protocols, effectively disallows the "href" 475 // attribute globally. 476 { 477 AttributePolicy urlAttributePolicy; 478 if (allowedProtocols.size() == 3 479 && allowedProtocols.contains("mailto") 480 && allowedProtocols.contains("http") 481 && allowedProtocols.contains("https")) { 482 urlAttributePolicy = StandardUrlAttributePolicy.INSTANCE; 483 } else { 484 urlAttributePolicy = new FilterUrlByProtocolAttributePolicy( 485 allowedProtocols); 486 } 487 Set<String> toGuard = Sets.newLinkedHashSet(URL_ATTRIBUTE_NAMES); 488 for (String urlAttributeName : URL_ATTRIBUTE_NAMES) { 489 if (globalAttrPolicies.containsKey(urlAttributeName)) { 490 toGuard.remove(urlAttributeName); 491 globalAttrPolicies.put(urlAttributeName, AttributePolicy.Util.join( 492 urlAttributePolicy, globalAttrPolicies.get(urlAttributeName))); 493 } 494 } 495 // Implement guards not implemented on global policies in the per-element 496 // policy maps. 497 for (Map.Entry<String, Map<String, AttributePolicy>> e 498 : attrPolicies.entrySet()) { 499 Map<String, AttributePolicy> policies = e.getValue(); 500 for (String urlAttributeName : toGuard) { 501 if (policies.containsKey(urlAttributeName)) { 502 policies.put(urlAttributeName, AttributePolicy.Util.join( 503 urlAttributePolicy, policies.get(urlAttributeName))); 504 } 505 } 506 } 507 } 508 509 ImmutableMap.Builder<String, ElementAndAttributePolicies> policiesBuilder 510 = ImmutableMap.builder(); 511 for (Map.Entry<String, ElementPolicy> e : elPolicies.entrySet()) { 512 String elementName = e.getKey(); 513 ElementPolicy elPolicy = e.getValue(); 514 if (ElementPolicy.REJECT_ALL_ELEMENT_POLICY.equals(elPolicy)) { 515 continue; 516 } 517 518 Map<String, AttributePolicy> elAttrPolicies 519 = attrPolicies.get(elementName); 520 if (elAttrPolicies == null) { elAttrPolicies = ImmutableMap.of(); } 521 ImmutableMap.Builder<String, AttributePolicy> attrs 522 = ImmutableMap.builder(); 523 for (Map.Entry<String, AttributePolicy> ape : elAttrPolicies.entrySet()) { 524 String attributeName = ape.getKey(); 525 if (globalAttrPolicies.containsKey(attributeName)) { continue; } 526 AttributePolicy policy = ape.getValue(); 527 if (!AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY.equals(policy)) { 528 attrs.put(attributeName, policy); 529 } 530 } 531 for (Map.Entry<String, AttributePolicy> ape 532 : globalAttrPolicies.entrySet()) { 533 String attributeName = ape.getKey(); 534 AttributePolicy policy = AttributePolicy.Util.join( 535 elAttrPolicies.get(attributeName), ape.getValue()); 536 if (!AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY.equals(policy)) { 537 attrs.put(attributeName, policy); 538 } 539 } 540 541 policiesBuilder.put( 542 elementName, 543 new ElementAndAttributePolicies( 544 elementName, 545 elPolicy, attrs.build(), skipIfEmpty.contains(elementName))); 546 } 547 return compiledPolicies = policiesBuilder.build(); 548 } 549 550 /** 551 * Builds the relationship between attributes, the values that they may have, 552 * and the elements on which they may appear. 553 * 554 * @author Mike Samuel 555 */ 556 public final class AttributeBuilder { 557 private final List<String> attributeNames; 558 private AttributePolicy policy = AttributePolicy.IDENTITY_ATTRIBUTE_POLICY; 559 560 AttributeBuilder(List<? extends String> attributeNames) { 561 this.attributeNames = ImmutableList.copyOf(attributeNames); 562 } 563 564 /** 565 * Filters and/or transforms the attribute values 566 * allowed by later {@code allow*} calls. 567 * Multiple calls to {@code matching} are combined so that the policies 568 * receive the value in order, each seeing the value after any 569 * transformation by a previous policy. 570 */ 571 public AttributeBuilder matching(AttributePolicy policy) { 572 this.policy = AttributePolicy.Util.join(this.policy, policy); 573 return this; 574 } 575 576 /** 577 * Restrict the values allowed by later {@code allow*} calls to those 578 * matching the pattern. 579 * Multiple calls to {@code matching} are combined to restrict to the 580 * intersection of possible matched values. 581 */ 582 public AttributeBuilder matching(final Pattern pattern) { 583 return matching(new AttributePolicy() { 584 public @Nullable String apply( 585 String elementName, String attributeName, String value) { 586 return pattern.matcher(value).matches() ? value : null; 587 } 588 }); 589 } 590 591 /** 592 * Restrict the values allowed by later {@code allow*} calls to those 593 * matching the given predicate. 594 * Multiple calls to {@code matching} are combined to restrict to the 595 * intersection of possible matched values. 596 */ 597 public AttributeBuilder matching( 598 final Predicate<? super String> filter) { 599 return matching(new AttributePolicy() { 600 public @Nullable String apply( 601 String elementName, String attributeName, String value) { 602 return filter.apply(value) ? value : null; 603 } 604 }); 605 } 606 607 /** 608 * Restrict the values allowed by later {@code allow*} calls to those 609 * supplied. 610 * Multiple calls to {@code matching} are combined to restrict to the 611 * intersection of possible matched values. 612 */ 613 public AttributeBuilder matching( 614 boolean ignoreCase, String... allowedValues) { 615 return matching(ignoreCase, ImmutableSet.copyOf(allowedValues)); 616 } 617 618 /** 619 * Restrict the values allowed by later {@code allow*} calls to those 620 * supplied. 621 * Multiple calls to {@code matching} are combined to restrict to the 622 * intersection of possible matched values. 623 */ 624 public AttributeBuilder matching( 625 final boolean ignoreCase, Set<? extends String> allowedValues) { 626 final ImmutableSet<String> allowed = ImmutableSet.copyOf(allowedValues); 627 return matching(new AttributePolicy() { 628 public @Nullable String apply( 629 String elementName, String attributeName, String value) { 630 if (ignoreCase) { value = Strings.toLowerCase(value); } 631 return allowed.contains(value) ? value : null; 632 } 633 }); 634 } 635 636 /** 637 * Allows the given attributes on any elements but filters the 638 * attributes' values based on previous calls to {@code matching(...)}. 639 * Global attribute policies are applied after element specific policies. 640 * Be careful of using this with attributes like <code>type</code> which 641 * have different meanings on different attributes. 642 * Also be careful of allowing globally attributes like <code>href</code> 643 * which can have more far-reaching effects on tags like 644 * <code><base></code> and <code><link></code> than on 645 * <code><a></code> because in the former, they have an effect without 646 * user interaction and can change the behavior of the current page. 647 */ 648 public HtmlPolicyBuilder globally() { 649 return HtmlPolicyBuilder.this.allowAttributesGlobally( 650 policy, attributeNames); 651 } 652 653 /** 654 * Allows the named attributes on the given elements but filters the 655 * attributes' values based on previous calls to {@code matching(...)}. 656 */ 657 public HtmlPolicyBuilder onElements(String... elementNames) { 658 ImmutableList.Builder<String> b = ImmutableList.builder(); 659 for (String elementName : elementNames) { 660 b.add(HtmlLexer.canonicalName(elementName)); 661 } 662 return HtmlPolicyBuilder.this.allowAttributesOnElements( 663 policy, attributeNames, b.build()); 664 } 665 } 666 667 } 668 669 final class Factory 670 implements Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> { 671 private final ImmutableMap<String, ElementAndAttributePolicies> policies; 672 private final boolean allowStyling; 673 674 Factory( 675 ImmutableMap<String, ElementAndAttributePolicies> policies, 676 boolean allowStyling) { 677 this.policies = policies; 678 this.allowStyling = allowStyling; 679 } 680 681 public HtmlSanitizer.Policy apply(HtmlStreamEventReceiver out) { 682 if (allowStyling) { 683 return new StylingPolicy(out, policies); 684 } else { 685 return new ElementAndAttributePolicyBasedSanitizerPolicy( 686 out, policies); 687 } 688 } 689 }