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    }