1// Copyright (c) 2011, Mike Samuel 2// All rights reserved. 3// 4// Redistribution and use in source and binary forms, with or without 5// modification, are permitted provided that the following conditions 6// are met: 7// 8// Redistributions of source code must retain the above copyright 9// notice, this list of conditions and the following disclaimer. 10// Redistributions in binary form must reproduce the above copyright 11// notice, this list of conditions and the following disclaimer in the 12// documentation and/or other materials provided with the distribution. 13// Neither the name of the OWASP nor the names of its contributors may 14// be used to endorse or promote products derived from this software 15// without specific prior written permission. 16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 19// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 20// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 26// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27// POSSIBILITY OF SUCH DAMAGE. 28 29package org.owasp.html; 30 31import java.util.Map; 32 33import javax.annotation.Nonnull; 34import javax.annotation.Nullable; 35import javax.annotation.concurrent.Immutable; 36import javax.annotation.concurrent.ThreadSafe; 37 38import com.google.common.base.Function; 39import com.google.common.collect.ImmutableMap; 40import com.google.common.collect.ImmutableSet; 41 42/** 43 * A factory that can be used to link a sanitizer to an output receiver and that 44 * provides a convenient <code>{@link PolicyFactory#sanitize sanitize}</code> 45 * method and a <code>{@link PolicyFactory#and and}</code> method to compose 46 * policies. 47 * 48 * @author Mike Samuel <mikesamuel@gmail.com> 49 */ 50@ThreadSafe 51@Immutable 52@TCB 53public final class PolicyFactory 54 implements Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> { 55 56 private final ImmutableMap<String, ElementAndAttributePolicies> policies; 57 private final ImmutableMap<String, AttributePolicy> globalAttrPolicies; 58 private final ImmutableSet<String> textContainers; 59 60 PolicyFactory( 61 ImmutableMap<String, ElementAndAttributePolicies> policies, 62 ImmutableSet<String> textContainers, 63 ImmutableMap<String, AttributePolicy> globalAttrPolicies) { 64 this.policies = policies; 65 this.textContainers = textContainers; 66 this.globalAttrPolicies = globalAttrPolicies; 67 } 68 69 /** Produces a sanitizer that emits tokens to {@code out}. */ 70 public HtmlSanitizer.Policy apply(@Nonnull HtmlStreamEventReceiver out) { 71 return new ElementAndAttributePolicyBasedSanitizerPolicy( 72 out, policies, textContainers); 73 } 74 75 /** 76 * Produces a sanitizer that emits tokens to {@code out} and that notifies 77 * any {@code listener} of any dropped tags and attributes. 78 * @param out a renderer that receives approved tokens only. 79 * @param listener if non-null, receives notifications of tags and attributes 80 * that were rejected by the policy. This may tie into intrusion 81 * detection systems. 82 * @param context if {@code (listener != null)} then the context value passed 83 * with notifications. This can be used to let the listener know from 84 * which connection or request the questionable HTML was received. 85 */ 86 public <CTX> HtmlSanitizer.Policy apply( 87 HtmlStreamEventReceiver out, @Nullable HtmlChangeListener<CTX> listener, 88 @Nullable CTX context) { 89 if (listener == null) { 90 return apply(out); 91 } else { 92 HtmlChangeReporter<CTX> r = new HtmlChangeReporter<CTX>( 93 out, listener, context); 94 r.setPolicy(apply(r.getWrappedRenderer())); 95 return r.getWrappedPolicy(); 96 } 97 } 98 99 /** A convenience function that sanitizes a string of HTML. */ 100 public String sanitize(@Nullable String html) { 101 return sanitize(html, null, null); 102 } 103 104 /** 105 * A convenience function that sanitizes a string of HTML and reports 106 * the names of rejected element and attributes to listener. 107 * @param html the string of HTML to sanitize. 108 * @param listener if non-null, receives notifications of tags and attributes 109 * that were rejected by the policy. This may tie into intrusion 110 * detection systems. 111 * @param context if {@code (listener != null)} then the context value passed 112 * with notifications. This can be used to let the listener know from 113 * which connection or request the questionable HTML was received. 114 * @return a string of HTML that complies with this factory's policy. 115 */ 116 public <CTX> String sanitize( 117 @Nullable String html, 118 @Nullable HtmlChangeListener<CTX> listener, @Nullable CTX context) { 119 if (html == null) { return ""; } 120 StringBuilder out = new StringBuilder(html.length()); 121 HtmlSanitizer.sanitize( 122 html, 123 apply(HtmlStreamRenderer.create(out, Handler.DO_NOTHING), 124 listener, context)); 125 return out.toString(); 126 } 127 128 /** 129 * Produces a factory that allows the union of the grants, and intersects 130 * policies where they overlap on a particular granted attribute or element 131 * name. 132 */ 133 public PolicyFactory and(PolicyFactory f) { 134 ImmutableMap.Builder<String, ElementAndAttributePolicies> b 135 = ImmutableMap.builder(); 136 // Merge this and f into a map of element names to attribute policies. 137 for (Map.Entry<String, ElementAndAttributePolicies> e 138 : policies.entrySet()) { 139 String elName = e.getKey(); 140 ElementAndAttributePolicies p = e.getValue(); 141 ElementAndAttributePolicies q = f.policies.get(elName); 142 if (q != null) { 143 p = p.and(q); 144 } else { 145 // Mix in any globals that are not already taken into account in this. 146 p = p.andGlobals(f.globalAttrPolicies); 147 } 148 b.put(elName, p); 149 } 150 // Handle keys that are in f but not in this. 151 for (Map.Entry<String, ElementAndAttributePolicies> e 152 : f.policies.entrySet()) { 153 String elName = e.getKey(); 154 if (!policies.containsKey(elName)) { 155 ElementAndAttributePolicies p = e.getValue(); 156 // Mix in any globals that are not already taken into account in this. 157 p = p.andGlobals(f.globalAttrPolicies); 158 b.put(elName, p); 159 } 160 } 161 ImmutableSet<String> textContainers; 162 if (this.textContainers.containsAll(f.textContainers)) { 163 textContainers = this.textContainers; 164 } else if (f.textContainers.containsAll(this.textContainers)) { 165 textContainers = f.textContainers; 166 } else { 167 textContainers = ImmutableSet.<String>builder() 168 .addAll(this.textContainers) 169 .addAll(f.textContainers) 170 .build(); 171 } 172 ImmutableMap<String, AttributePolicy> allGlobalAttrPolicies; 173 if (f.globalAttrPolicies.isEmpty()) { 174 allGlobalAttrPolicies = this.globalAttrPolicies; 175 } else if (this.globalAttrPolicies.isEmpty()) { 176 allGlobalAttrPolicies = f.globalAttrPolicies; 177 } else { 178 ImmutableMap.Builder<String, AttributePolicy> ab = ImmutableMap.builder(); 179 for (Map.Entry<String, AttributePolicy> e 180 : this.globalAttrPolicies.entrySet()) { 181 String attrName = e.getKey(); 182 ab.put( 183 attrName, 184 AttributePolicy.Util.join( 185 e.getValue(), f.globalAttrPolicies.get(attrName))); 186 } 187 for (Map.Entry<String, AttributePolicy> e 188 : f.globalAttrPolicies.entrySet()) { 189 String attrName = e.getKey(); 190 if (!this.globalAttrPolicies.containsKey(attrName)) { 191 ab.put(attrName, e.getValue()); 192 } 193 } 194 allGlobalAttrPolicies = ab.build(); 195 } 196 return new PolicyFactory(b.build(), textContainers, allGlobalAttrPolicies); 197 } 198} 199