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 
29 package org.owasp.html;
30 
31 import java.util.List;
32 
33 import javax.annotation.Nullable;
34 
35 import com.google.common.annotations.VisibleForTesting;
36 import 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
44 final class StylingPolicy implements AttributePolicy {
45 
46   private final CssSchema cssSchema;
47 
StylingPolicy(CssSchema cssSchema)48   StylingPolicy(CssSchema cssSchema) {
49     this.cssSchema = cssSchema;
50   }
51 
apply( String elementName, String attributeName, String value)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
sanitizeCssProperties(String style)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
113             ? cssSchema.forKey(key)
114             : CssSchema.DISALLOWED;
115         if (cssProperty != CssSchema.DISALLOWED) {
116           emitToken(token);
117         }
118       }
119 
120       public void quotedString(String token) {
121         closeQuotedIdents();
122         // The contents of a quoted string could be treated as
123         // 1. a run of space-separated words, as in a font family name,
124         // 2. as a URL,
125         // 3. as plain text content as in a list-item bullet,
126         // 4. or it could be ambiguous as when multiple bits are set.
127         int meaning =
128             cssProperty.bits
129             & (CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_URL);
130         if ((meaning & (meaning - 1)) == 0) {  // meaning is unambiguous
131           if (meaning == CssSchema.BIT_UNRESERVED_WORD
132               && token.length() > 2
133               && isAlphanumericOrSpace(token, 1, token.length() - 1)) {
134             emitToken(Strings.toLowerCase(token));
135           } else if (meaning == CssSchema.BIT_URL) {
136             // convert to a URL token and hand-off to the appropriate method
137             // url("url(" + token + ")");  // TODO: %-encode properly
138           }
139         }
140       }
141 
142       public void quantity(String token) {
143         int test = token.startsWith("-")
144             ? CssSchema.BIT_NEGATIVE : CssSchema.BIT_QUANTITY;
145         if ((cssProperty.bits & test) != 0
146             // font-weight uses 100, 200, 300, etc.
147             || cssProperty.literals.contains(token)) {
148           emitToken(token);
149         }
150       }
151 
152       public void punctuation(String token) {
153         closeQuotedIdents();
154         if (cssProperty.literals.contains(token)) {
155           emitToken(token);
156         }
157       }
158 
159       private static final int IDENT_TO_STRING =
160           CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_STRING;
161       public void identifier(String token) {
162         token = Strings.toLowerCase(token);
163         if (cssProperty.literals.contains(token)) {
164           emitToken(token);
165         } else if ((cssProperty.bits & IDENT_TO_STRING) == IDENT_TO_STRING) {
166           if (!inQuotedIdents) {
167             inQuotedIdents = true;
168             if (hasTokens) { sanitizedCss.append(' '); }
169             sanitizedCss.append('\'');
170             hasTokens = true;
171           } else {
172             sanitizedCss.append(' ');
173           }
174           sanitizedCss.append(Strings.toLowerCase(token));
175         }
176       }
177 
178       public void hash(String token) {
179         closeQuotedIdents();
180         if ((cssProperty.bits & CssSchema.BIT_HASH_VALUE) != 0) {
181           emitToken(Strings.toLowerCase(token));
182         }
183       }
184 
185       public void endProperty() {
186         if (!hasTokens) {
187           sanitizedCss.setLength(propertyStart);
188         } else {
189           closeQuotedIdents();
190         }
191       }
192 
193       public void endFunction(String token) {
194         if (cssProperty != CssSchema.DISALLOWED) { emitToken(")"); }
195         cssProperty = cssProperties.remove(cssProperties.size() - 1);
196       }
197     });
198     return sanitizedCss.length() == 0 ? null : sanitizedCss.toString();
199   }
200 
isAlphanumericOrSpace( String token, int start, int end)201   private static boolean isAlphanumericOrSpace(
202       String token, int start, int end) {
203     for (int i = start; i < end; ++i) {
204       char ch = token.charAt(i);
205       if (ch <= 0x20) {
206         if (ch != '\t' && ch != ' ') {
207           return false;
208         }
209       } else {
210         int chLower = ch | 32;
211         if (!(('0' <= chLower && chLower <= '9')
212               || ('a' <= chLower && chLower <= 'z'))) {
213           return false;
214         }
215       }
216     }
217     return true;
218   }
219 
220   @Override
equals(Object o)221   public boolean equals(Object o) {
222     return o != null && getClass() == o.getClass()
223         && cssSchema.equals(((StylingPolicy) o).cssSchema);
224   }
225 
226   @Override
hashCode()227   public int hashCode() {
228     return cssSchema.hashCode();
229   }
230 
231 }
232