blob: 1c448e71261123d96ff8e38c64268251a5c371b5 [file] [log] [blame]
// 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;
}
}