Home | History | Annotate | Download | only in html
      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 
     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
    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 
    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
    221   public boolean equals(Object o) {
    222     return o != null && getClass() == o.getClass()
    223         && cssSchema.equals(((StylingPolicy) o).cssSchema);
    224   }
    225 
    226   @Override
    227   public int hashCode() {
    228     return cssSchema.hashCode();
    229   }
    230 
    231 }
    232