StylingPolicy.java revision 1af054935066ae9db1476bef96ff224410edb1f4
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;
32
33import javax.annotation.Nullable;
34
35import com.google.common.annotations.VisibleForTesting;
36import com.google.common.collect.Lists;
37
38/**
39 * An HTML sanitizer policy that tries to preserve simple CSS by white-listing
40 * property values and splitting combo properties into multiple more specific
41 * ones to reduce the attack-surface.
42 */
43@TCB
44final class StylingPolicy implements AttributePolicy {
45
46  private final CssSchema cssSchema;
47
48  StylingPolicy(CssSchema cssSchema) {
49    this.cssSchema = cssSchema;
50  }
51
52  public @Nullable String apply(
53      String elementName, String attributeName, String value) {
54    return value != null ? sanitizeCssProperties(value) : null;
55  }
56
57  /**
58   * Lossy filtering of CSS properties that allows textual styling that affects
59   * layout, but does not allow breaking out of a clipping region, absolute
60   * positioning, image loading, tab index changes, or code execution.
61   *
62   * @return A sanitized version of the input.
63   */
64  @VisibleForTesting
65  String sanitizeCssProperties(String style) {
66    final StringBuilder sanitizedCss = new StringBuilder();
67    CssGrammar.parsePropertyGroup(style, new CssGrammar.PropertyHandler() {
68      CssSchema.Property cssProperty = CssSchema.DISALLOWED;
69      List<CssSchema.Property> cssProperties = null;
70      int propertyStart = 0;
71      boolean hasTokens;
72      boolean inQuotedIdents;
73
74      private void emitToken(String token) {
75        closeQuotedIdents();
76        if (hasTokens) { sanitizedCss.append(' '); }
77        sanitizedCss.append(token);
78        hasTokens = true;
79      }
80
81      private void closeQuotedIdents() {
82        if (inQuotedIdents) {
83          sanitizedCss.append('\'');
84          inQuotedIdents = false;
85        }
86      }
87
88      public void url(String token) {
89        closeQuotedIdents();
90        //if ((schema.bits & CssSchema.BIT_URL) != 0) {
91        // TODO: sanitize the URL.
92        //}
93      }
94
95      public void startProperty(String propertyName) {
96        if (cssProperties != null) { cssProperties.clear(); }
97        cssProperty = cssSchema.forKey(propertyName);
98        hasTokens = false;
99        propertyStart = sanitizedCss.length();
100        if (sanitizedCss.length() != 0) {
101          sanitizedCss.append(';');
102        }
103        sanitizedCss.append(propertyName).append(':');
104      }
105
106      public void startFunction(String token) {
107        closeQuotedIdents();
108        if (cssProperties == null) { cssProperties = Lists.newArrayList(); }
109        cssProperties.add(cssProperty);
110        token = Strings.toLowerCase(token);
111        String key = cssProperty.fnKeys.get(token);
112        cssProperty = key != null ? cssSchema.forKey(key) : CssSchema.DISALLOWED;
113        if (cssProperty != CssSchema.DISALLOWED) {
114          emitToken(token);
115        }
116      }
117
118      public void quotedString(String token) {
119        closeQuotedIdents();
120        int meaning =
121            cssProperty.bits & (CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_URL);
122        if ((meaning & (meaning - 1)) == 0) {  // meaning is unambiguous
123          if (meaning == CssSchema.BIT_UNRESERVED_WORD
124              && token.length() > 2
125              && isAlphanumericOrSpace(token, 1, token.length() - 1)) {
126            emitToken(Strings.toLowerCase(token));
127          } else if (meaning == CssSchema.BIT_URL) {
128            // url("url(" + token + ")");  // TODO: %-encode properly
129          }
130        }
131      }
132
133      public void quantity(String token) {
134        int test = token.startsWith("-")
135            ? CssSchema.BIT_NEGATIVE : CssSchema.BIT_QUANTITY;
136        if ((cssProperty.bits & test) != 0
137            // font-weight uses 100, 200, 300, etc.
138            || cssProperty.literals.contains(token)) {
139          emitToken(token);
140        }
141      }
142
143      public void punctuation(String token) {
144        closeQuotedIdents();
145        if (cssProperty.literals.contains(token)) {
146          emitToken(token);
147        }
148      }
149
150      private static final int IDENT_TO_STRING =
151          CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_STRING;
152      public void identifier(String token) {
153        token = Strings.toLowerCase(token);
154        if (cssProperty.literals.contains(token)) {
155          emitToken(token);
156        } else if ((cssProperty.bits & IDENT_TO_STRING) == IDENT_TO_STRING) {
157          if (!inQuotedIdents) {
158            inQuotedIdents = true;
159            if (hasTokens) { sanitizedCss.append(' '); }
160            sanitizedCss.append('\'');
161            hasTokens = true;
162          } else {
163            sanitizedCss.append(' ');
164          }
165          sanitizedCss.append(Strings.toLowerCase(token));
166        }
167      }
168
169      public void hash(String token) {
170        closeQuotedIdents();
171        if ((cssProperty.bits & CssSchema.BIT_HASH_VALUE) != 0) {
172          emitToken(Strings.toLowerCase(token));
173        }
174      }
175
176      public void endProperty() {
177        if (!hasTokens) {
178          sanitizedCss.setLength(propertyStart);
179        } else {
180          closeQuotedIdents();
181        }
182      }
183
184      public void endFunction(String token) {
185        if (cssProperty != CssSchema.DISALLOWED) { emitToken(")"); }
186        cssProperty = cssProperties.remove(cssProperties.size() - 1);
187      }
188    });
189    return sanitizedCss.length() == 0 ? null : sanitizedCss.toString();
190  }
191
192  private static boolean isAlphanumericOrSpace(
193      String token, int start, int end) {
194    for (int i = start; i < end; ++i) {
195      char ch = token.charAt(i);
196      if (ch <= 0x20) {
197        if (ch != '\t' && ch != ' ') {
198          return false;
199        }
200      } else {
201        int chLower = ch | 32;
202        if (!(('0' <= chLower && chLower <= '9')
203              || ('a' <= chLower && chLower <= 'z'))) {
204          return false;
205        }
206      }
207    }
208    return true;
209  }
210}
211