| // Copyright (c) 2011, Mike Samuel |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions |
| // are met: |
| // |
| // Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // Neither the name of the OWASP nor the names of its contributors may |
| // be used to endorse or promote products derived from this software |
| // without specific prior written permission. |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
| // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
| // COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
| // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
| // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
| // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| // POSSIBILITY OF SUCH DAMAGE. |
| |
| package org.owasp.html; |
| |
| import java.util.List; |
| |
| import javax.annotation.Nullable; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.Lists; |
| |
| /** |
| * An HTML sanitizer policy that tries to preserve simple CSS by white-listing |
| * property values and splitting combo properties into multiple more specific |
| * ones to reduce the attack-surface. |
| */ |
| @TCB |
| final class StylingPolicy implements AttributePolicy { |
| |
| private final CssSchema cssSchema; |
| |
| StylingPolicy(CssSchema cssSchema) { |
| this.cssSchema = cssSchema; |
| } |
| |
| public @Nullable String apply( |
| String elementName, String attributeName, String value) { |
| return value != null ? sanitizeCssProperties(value) : null; |
| } |
| |
| /** |
| * Lossy filtering of CSS properties that allows textual styling that affects |
| * layout, but does not allow breaking out of a clipping region, absolute |
| * positioning, image loading, tab index changes, or code execution. |
| * |
| * @return A sanitized version of the input. |
| */ |
| @VisibleForTesting |
| String sanitizeCssProperties(String style) { |
| final StringBuilder sanitizedCss = new StringBuilder(); |
| CssGrammar.parsePropertyGroup(style, new CssGrammar.PropertyHandler() { |
| CssSchema.Property cssProperty = CssSchema.DISALLOWED; |
| List<CssSchema.Property> cssProperties = null; |
| int propertyStart = 0; |
| boolean hasTokens; |
| boolean inQuotedIdents; |
| |
| private void emitToken(String token) { |
| closeQuotedIdents(); |
| if (hasTokens) { sanitizedCss.append(' '); } |
| sanitizedCss.append(token); |
| hasTokens = true; |
| } |
| |
| private void closeQuotedIdents() { |
| if (inQuotedIdents) { |
| sanitizedCss.append('\''); |
| inQuotedIdents = false; |
| } |
| } |
| |
| public void url(String token) { |
| closeQuotedIdents(); |
| //if ((schema.bits & CssSchema.BIT_URL) != 0) { |
| // TODO: sanitize the URL. |
| //} |
| } |
| |
| public void startProperty(String propertyName) { |
| if (cssProperties != null) { cssProperties.clear(); } |
| cssProperty = cssSchema.forKey(propertyName); |
| hasTokens = false; |
| propertyStart = sanitizedCss.length(); |
| if (sanitizedCss.length() != 0) { |
| sanitizedCss.append(';'); |
| } |
| sanitizedCss.append(propertyName).append(':'); |
| } |
| |
| public void startFunction(String token) { |
| closeQuotedIdents(); |
| if (cssProperties == null) { cssProperties = Lists.newArrayList(); } |
| cssProperties.add(cssProperty); |
| token = Strings.toLowerCase(token); |
| String key = cssProperty.fnKeys.get(token); |
| cssProperty = key != null |
| ? cssSchema.forKey(key) |
| : CssSchema.DISALLOWED; |
| if (cssProperty != CssSchema.DISALLOWED) { |
| emitToken(token); |
| } |
| } |
| |
| public void quotedString(String token) { |
| closeQuotedIdents(); |
| // The contents of a quoted string could be treated as |
| // 1. a run of space-separated words, as in a font family name, |
| // 2. as a URL, |
| // 3. as plain text content as in a list-item bullet, |
| // 4. or it could be ambiguous as when multiple bits are set. |
| int meaning = |
| cssProperty.bits |
| & (CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_URL); |
| if ((meaning & (meaning - 1)) == 0) { // meaning is unambiguous |
| if (meaning == CssSchema.BIT_UNRESERVED_WORD |
| && token.length() > 2 |
| && isAlphanumericOrSpace(token, 1, token.length() - 1)) { |
| emitToken(Strings.toLowerCase(token)); |
| } else if (meaning == CssSchema.BIT_URL) { |
| // convert to a URL token and hand-off to the appropriate method |
| // url("url(" + token + ")"); // TODO: %-encode properly |
| } |
| } |
| } |
| |
| public void quantity(String token) { |
| int test = token.startsWith("-") |
| ? CssSchema.BIT_NEGATIVE : CssSchema.BIT_QUANTITY; |
| if ((cssProperty.bits & test) != 0 |
| // font-weight uses 100, 200, 300, etc. |
| || cssProperty.literals.contains(token)) { |
| emitToken(token); |
| } |
| } |
| |
| public void punctuation(String token) { |
| closeQuotedIdents(); |
| if (cssProperty.literals.contains(token)) { |
| emitToken(token); |
| } |
| } |
| |
| private static final int IDENT_TO_STRING = |
| CssSchema.BIT_UNRESERVED_WORD | CssSchema.BIT_STRING; |
| public void identifier(String token) { |
| token = Strings.toLowerCase(token); |
| if (cssProperty.literals.contains(token)) { |
| emitToken(token); |
| } else if ((cssProperty.bits & IDENT_TO_STRING) == IDENT_TO_STRING) { |
| if (!inQuotedIdents) { |
| inQuotedIdents = true; |
| if (hasTokens) { sanitizedCss.append(' '); } |
| sanitizedCss.append('\''); |
| hasTokens = true; |
| } else { |
| sanitizedCss.append(' '); |
| } |
| sanitizedCss.append(Strings.toLowerCase(token)); |
| } |
| } |
| |
| public void hash(String token) { |
| closeQuotedIdents(); |
| if ((cssProperty.bits & CssSchema.BIT_HASH_VALUE) != 0) { |
| emitToken(Strings.toLowerCase(token)); |
| } |
| } |
| |
| public void endProperty() { |
| if (!hasTokens) { |
| sanitizedCss.setLength(propertyStart); |
| } else { |
| closeQuotedIdents(); |
| } |
| } |
| |
| public void endFunction(String token) { |
| if (cssProperty != CssSchema.DISALLOWED) { emitToken(")"); } |
| cssProperty = cssProperties.remove(cssProperties.size() - 1); |
| } |
| }); |
| return sanitizedCss.length() == 0 ? null : sanitizedCss.toString(); |
| } |
| |
| private static boolean isAlphanumericOrSpace( |
| String token, int start, int end) { |
| for (int i = start; i < end; ++i) { |
| char ch = token.charAt(i); |
| if (ch <= 0x20) { |
| if (ch != '\t' && ch != ' ') { |
| return false; |
| } |
| } else { |
| int chLower = ch | 32; |
| if (!(('0' <= chLower && chLower <= '9') |
| || ('a' <= chLower && chLower <= 'z'))) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| } |