ElementAndAttributePolicyBasedSanitizerPolicy.java revision 79b4c29af1261d95c663bdf0003b70cb0eb8000e
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.List;
32import java.util.ListIterator;
33
34import javax.annotation.Nullable;
35import javax.annotation.concurrent.NotThreadSafe;
36
37import com.google.common.collect.ImmutableMap;
38import com.google.common.collect.ImmutableSet;
39import com.google.common.collect.Lists;
40
41/**
42 * A sanitizer policy that applies element and attribute policies to tags.
43 */
44@TCB
45@NotThreadSafe
46class ElementAndAttributePolicyBasedSanitizerPolicy
47    implements HtmlSanitizer.Policy {
48  final ImmutableMap<String, ElementAndAttributePolicies> elAndAttrPolicies;
49  final ImmutableSet<String> allowedTextContainers;
50  private final HtmlStreamEventReceiver out;
51  /**
52   * True to skip textual content.  Used to ignore the content of embedded CDATA
53   * content that is not meant to be human-readable.
54   */
55  transient boolean skipText = true;
56  /**
57   * Alternating input names and adjusted names of elements opened by the
58   * caller.
59   */
60  private final List<String> openElementStack = Lists.newArrayList();
61
62  ElementAndAttributePolicyBasedSanitizerPolicy(
63      HtmlStreamEventReceiver out,
64      ImmutableMap<String, ElementAndAttributePolicies> elAndAttrPolicies,
65      ImmutableSet<String> allowedTextContainers) {
66    this.out = out;
67    this.elAndAttrPolicies = elAndAttrPolicies;
68    this.allowedTextContainers = allowedTextContainers;
69  }
70
71  static final ImmutableSet<String> SKIPPABLE_ELEMENT_CONTENT
72      = ImmutableSet.of(
73          "script", "style", "noscript", "nostyle", "noembed", "noframes",
74          "iframe", "object", "frame", "frameset", "title");
75
76  public void openDocument() {
77    skipText = false;
78    openElementStack.clear();
79    out.openDocument();
80  }
81
82  public void closeDocument() {
83    for (int i = openElementStack.size() - 1; i >= 0; i -= 2) {
84      String tagNameToClose = openElementStack.get(i);
85      if (tagNameToClose != null) {
86        out.closeTag(tagNameToClose);
87      }
88    }
89    openElementStack.clear();
90    skipText = true;
91    out.closeDocument();
92  }
93
94  public void text(String textChunk) {
95    if (!skipText) {
96      out.text(textChunk);
97    }
98  }
99
100  public void openTag(String elementName, List<String> attrs) {
101    // StylingPolicy repeats some of this code because it is more complicated
102    // to refactor it into multiple method bodies, so if you change this,
103    // check the override of it in that class.
104    ElementAndAttributePolicies policies = elAndAttrPolicies.get(elementName);
105    String adjustedElementName = applyPolicies(elementName, attrs, policies);
106    if (adjustedElementName != null
107        && !(attrs.isEmpty() && policies.skipIfEmpty)) {
108      writeOpenTag(policies, adjustedElementName, attrs);
109      return;
110    }
111    deferOpenTag(elementName);
112  }
113
114  final @Nullable String applyPolicies(
115      String elementName, List<String> attrs,
116      ElementAndAttributePolicies policies) {
117    String adjustedElementName;
118    if (policies != null) {
119      for (ListIterator<String> attrsIt = attrs.listIterator();
120           attrsIt.hasNext();) {
121        String name = attrsIt.next();
122        AttributePolicy attrPolicy
123            = policies.attrPolicies.get(name);
124        if (attrPolicy == null) {
125          attrsIt.remove();
126          attrsIt.next();
127          attrsIt.remove();
128        } else {
129          String value = attrsIt.next();
130          String adjustedValue = attrPolicy.apply(elementName, name, value);
131          if (adjustedValue == null) {
132            attrsIt.remove();
133            attrsIt.previous();
134            attrsIt.remove();
135          } else {
136            attrsIt.set(adjustedValue);
137          }
138        }
139      }
140      adjustedElementName = policies.elPolicy.apply(elementName, attrs);
141    } else {
142      adjustedElementName = null;
143    }
144    return adjustedElementName;
145  }
146
147  public void closeTag(String elementName) {
148    int n = openElementStack.size();
149    for (int i = n; i > 0;) {
150      i -= 2;
151      String openElementName = openElementStack.get(i);
152      if (elementName.equals(openElementName)) {
153        for (int j = n - 1; j > i; j -= 2) {
154          String tagNameToClose = openElementStack.get(j);
155          if (tagNameToClose != null) {
156            out.closeTag(tagNameToClose);
157          }
158        }
159        openElementStack.subList(i, n).clear();
160        break;
161      }
162    }
163    skipText = false;
164    for (int i = openElementStack.size() - 1; i >= 0; i -= 2) {
165      String adjustedName = openElementStack.get(i);
166      if (adjustedName != null) {
167        skipText = !(allowedTextContainers.contains(adjustedName));
168        break;
169      }
170    }
171  }
172
173  void writeOpenTag(
174      ElementAndAttributePolicies policies, String adjustedElementName,
175      List<String> attrs) {
176    if (!policies.isVoid) {
177      openElementStack.add(policies.elementName);
178      openElementStack.add(adjustedElementName);
179      skipText = !allowedTextContainers.contains(adjustedElementName);
180    }
181    out.openTag(adjustedElementName, attrs);
182  }
183
184  void deferOpenTag(String elementName) {
185    if (HtmlTextEscapingMode.isVoidElement(elementName)) {
186      openElementStack.add(elementName);
187      openElementStack.add(null);
188    }
189    skipText = SKIPPABLE_ELEMENT_CONTENT.contains(elementName);
190  }
191
192  void synthesizeOpenTag(String adjustedElementName, List<String> attrs) {
193    openElementStack.add(null);
194    openElementStack.add(adjustedElementName);
195    out.openTag(adjustedElementName, attrs);
196  }
197}