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