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