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.Map;
032
033 import javax.annotation.Nonnull;
034 import javax.annotation.Nullable;
035 import javax.annotation.concurrent.Immutable;
036 import javax.annotation.concurrent.ThreadSafe;
037
038 import com.google.common.base.Function;
039 import com.google.common.collect.ImmutableMap;
040 import com.google.common.collect.ImmutableSet;
041
042 /**
043 * A factory that can be used to link a sanitizer to an output receiver and that
044 * provides a convenient <code>{@link PolicyFactory#sanitize sanitize}</code>
045 * method and a <code>{@link PolicyFactory#and and}</code> method to compose
046 * policies.
047 *
048 * @author Mike Samuel <mikesamuel@gmail.com>
049 */
050 @ThreadSafe
051 @Immutable
052 @TCB
053 public final class PolicyFactory
054 implements Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> {
055
056 private final ImmutableMap<String, ElementAndAttributePolicies> policies;
057 private final ImmutableMap<String, AttributePolicy> globalAttrPolicies;
058 private final ImmutableSet<String> textContainers;
059
060 PolicyFactory(
061 ImmutableMap<String, ElementAndAttributePolicies> policies,
062 ImmutableSet<String> textContainers,
063 ImmutableMap<String, AttributePolicy> globalAttrPolicies) {
064 this.policies = policies;
065 this.textContainers = textContainers;
066 this.globalAttrPolicies = globalAttrPolicies;
067 }
068
069 /** Produces a sanitizer that emits tokens to {@code out}. */
070 public HtmlSanitizer.Policy apply(@Nonnull HtmlStreamEventReceiver out) {
071 return new ElementAndAttributePolicyBasedSanitizerPolicy(
072 out, policies, textContainers);
073 }
074
075 /**
076 * Produces a sanitizer that emits tokens to {@code out} and that notifies
077 * any {@code listener} of any dropped tags and attributes.
078 * @param out a renderer that receives approved tokens only.
079 * @param listener if non-null, receives notifications of tags and attributes
080 * that were rejected by the policy. This may tie into intrusion
081 * detection systems.
082 * @param context if {@code (listener != null)} then the context value passed
083 * with notifications. This can be used to let the listener know from
084 * which connection or request the questionable HTML was received.
085 */
086 public <CTX> HtmlSanitizer.Policy apply(
087 HtmlStreamEventReceiver out, @Nullable HtmlChangeListener<CTX> listener,
088 @Nullable CTX context) {
089 if (listener == null) {
090 return apply(out);
091 } else {
092 HtmlChangeReporter<CTX> r = new HtmlChangeReporter<CTX>(
093 out, listener, context);
094 r.setPolicy(apply(r.getWrappedRenderer()));
095 return r.getWrappedPolicy();
096 }
097 }
098
099 /** 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 }