Skip to content

Commit f76a8a7

Browse files
committed
issue 72: CSS url handling unimplemented. Implemented it and enabled via .allowUrlsInStyles(...) to prevent changing the meaning of existing policies
1 parent 68b1c7c commit f76a8a7

File tree

5 files changed

+208
-14
lines changed

5 files changed

+208
-14
lines changed

src/main/java/org/owasp/html/HtmlPolicyBuilder.java

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import javax.annotation.Nullable;
3737
import javax.annotation.concurrent.NotThreadSafe;
3838

39+
import com.google.common.base.Function;
3940
import com.google.common.base.Predicate;
4041
import com.google.common.collect.ImmutableList;
4142
import com.google.common.collect.ImmutableMap;
@@ -176,6 +177,9 @@ public class HtmlPolicyBuilder {
176177
HtmlStreamEventProcessor.Processors.IDENTITY;
177178
private HtmlStreamEventProcessor preprocessor =
178179
HtmlStreamEventProcessor.Processors.IDENTITY;
180+
private CssSchema stylingPolicySchema = null;
181+
private AttributePolicy styleUrlPolicy =
182+
AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY;
179183
private boolean requireRelNofollowOnLinks;
180184

181185
/**
@@ -444,8 +448,34 @@ public HtmlPolicyBuilder allowStyling() {
444448
*/
445449
public HtmlPolicyBuilder allowStyling(CssSchema whitelist) {
446450
invalidateCompiledState();
447-
allowAttributesGlobally(
448-
new StylingPolicy(whitelist), ImmutableList.of("style"));
451+
452+
// Allow the style attribute, and then we will fix it up later. This allows
453+
// us to attach the final URL policy to the style attribute policy, while
454+
// still not allowing styles when allowStyling is followed by a call to
455+
// disallowAttributesGlobally("style").
456+
this.allowAttributesGlobally(
457+
AttributePolicy.IDENTITY_ATTRIBUTE_POLICY, ImmutableList.of("style"));
458+
459+
this.stylingPolicySchema =
460+
this.stylingPolicySchema == null
461+
? whitelist
462+
: CssSchema.union(stylingPolicySchema, whitelist);
463+
return this;
464+
}
465+
466+
/**
467+
* Allow URLs in CSS styles.
468+
* URLs in CSS are typically loaded without user-interaction, the way links
469+
* are, so a greater degree of scrutiny is warranted.
470+
*
471+
* @param newStyleUrlPolicy receives URLs from the CSS that pass the allowed
472+
* protocol policies, and may return null to veto the URL or the URL
473+
* to use. URLs will be reported as content in {@code <img src=...>}.
474+
*/
475+
public HtmlPolicyBuilder allowUrlsInStyles(
476+
AttributePolicy newStyleUrlPolicy) {
477+
this.invalidateCompiledState();
478+
this.styleUrlPolicy = newStyleUrlPolicy;
449479
return this;
450480
}
451481

@@ -583,8 +613,9 @@ private ImmutableMap<String, ElementAndAttributePolicies> compilePolicies() {
583613
// allowing the attribute "href" globally with the identity policy but
584614
// not white-listing any protocols, effectively disallows the "href"
585615
// attribute globally.
616+
StylingPolicy stylingPolicy = null;
586617
{
587-
AttributePolicy urlAttributePolicy;
618+
final AttributePolicy urlAttributePolicy;
588619
if (allowedProtocols.size() == 3
589620
&& allowedProtocols.contains("mailto")
590621
&& allowedProtocols.contains("http")
@@ -594,6 +625,19 @@ private ImmutableMap<String, ElementAndAttributePolicies> compilePolicies() {
594625
urlAttributePolicy = new FilterUrlByProtocolAttributePolicy(
595626
allowedProtocols);
596627
}
628+
629+
if (this.stylingPolicySchema != null) {
630+
final AttributePolicy styleUrlPolicyFinal = AttributePolicy.Util.join(
631+
styleUrlPolicy, urlAttributePolicy);
632+
stylingPolicy = new StylingPolicy(
633+
stylingPolicySchema,
634+
new Function<String, String>() {
635+
public String apply(String url) {
636+
return styleUrlPolicyFinal.apply("img", "src", url);
637+
}
638+
});
639+
}
640+
597641
Set<String> toGuard = Sets.newLinkedHashSet(URL_ATTRIBUTE_NAMES);
598642
for (String urlAttributeName : URL_ATTRIBUTE_NAMES) {
599643
if (globalAttrPolicies.containsKey(urlAttributeName)) {
@@ -636,6 +680,9 @@ private ImmutableMap<String, ElementAndAttributePolicies> compilePolicies() {
636680
// twice. ImmutableMap.Builder hates that.
637681
if (globalAttrPolicies.containsKey(attributeName)) { continue; }
638682
AttributePolicy policy = ape.getValue();
683+
if ("style".equals(attributeName)) {
684+
policy = AttributePolicy.Util.join(policy, stylingPolicy);
685+
}
639686
if (!AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY.equals(policy)) {
640687
attrs.put(attributeName, policy);
641688
}
@@ -645,6 +692,9 @@ private ImmutableMap<String, ElementAndAttributePolicies> compilePolicies() {
645692
String attributeName = ape.getKey();
646693
AttributePolicy policy = AttributePolicy.Util.join(
647694
elAttrPolicies.get(attributeName), ape.getValue());
695+
if ("style".equals(attributeName)) {
696+
policy = AttributePolicy.Util.join(policy, stylingPolicy);
697+
}
648698
if (!AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY.equals(policy)) {
649699
attrs.put(attributeName, policy);
650700
}

src/main/java/org/owasp/html/StylingPolicy.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import javax.annotation.Nullable;
3434

3535
import com.google.common.annotations.VisibleForTesting;
36+
import com.google.common.base.Function;
3637
import com.google.common.collect.Lists;
3738

3839
/**
@@ -44,9 +45,11 @@
4445
final class StylingPolicy implements AttributePolicy {
4546

4647
final CssSchema cssSchema;
48+
final Function<String, String> urlRewriter;
4749

48-
StylingPolicy(CssSchema cssSchema) {
50+
StylingPolicy(CssSchema cssSchema, Function<String, String> urlRewriter) {
4951
this.cssSchema = cssSchema;
52+
this.urlRewriter = urlRewriter;
5053
}
5154

5255
public @Nullable String apply(
@@ -85,11 +88,27 @@ private void closeQuotedIdents() {
8588
}
8689
}
8790

91+
private void sanitizeAndAppendUrl(String urlContent) {
92+
if (urlContent.length() < 1024) {
93+
String rewrittenUrl = urlRewriter.apply(urlContent);
94+
if (rewrittenUrl != null && !rewrittenUrl.isEmpty()) {
95+
if (hasTokens) { sanitizedCss.append(' '); }
96+
sanitizedCss.append("url('").append(rewrittenUrl).append("')");
97+
hasTokens = true;
98+
}
99+
}
100+
}
101+
88102
public void url(String token) {
89103
closeQuotedIdents();
90-
//if ((schema.bits & CssSchema.BIT_URL) != 0) {
91-
// TODO: sanitize the URL.
92-
//}
104+
if (cssProperty != null) {
105+
if ((cssProperty.bits & CssSchema.BIT_URL) != 0) {
106+
String urlContent = CssGrammar.cssContent(
107+
Strings.stripHtmlSpaces( // TODO: css spaces
108+
token.substring(4, token.length() - 1)));
109+
sanitizeAndAppendUrl(urlContent);
110+
}
111+
}
93112
}
94113

95114
public void startProperty(String propertyName) {
@@ -134,7 +153,7 @@ && isAlphanumericOrSpace(token, 1, token.length() - 1)) {
134153
emitToken(Strings.toLowerCase(token));
135154
} else if (meaning == CssSchema.BIT_URL) {
136155
// convert to a URL token and hand-off to the appropriate method
137-
// url("url(" + token + ")"); // TODO: %-encode properly
156+
sanitizeAndAppendUrl(CssGrammar.cssContent(token));
138157
}
139158
}
140159
}
@@ -227,5 +246,4 @@ public boolean equals(Object o) {
227246
public int hashCode() {
228247
return cssSchema.hashCode();
229248
}
230-
231249
}

src/test/java/org/owasp/html/HtmlPolicyBuilderTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,95 @@ public String toString() {
479479

480480
}
481481

482+
@Test
483+
public static final void testBackgroundImageWithUrl() {
484+
PolicyFactory policy = new HtmlPolicyBuilder()
485+
.allowStandardUrlProtocols()
486+
.allowStyling()
487+
.allowUrlsInStyles(AttributePolicy.IDENTITY_ATTRIBUTE_POLICY)
488+
.allowElements("div")
489+
.toFactory();
490+
String unsafeHtml = policy.sanitize(
491+
"<html><head><title>test</title></head><body>" +
492+
"<div style='"
493+
+ "color: red; background-image: "
494+
+ "url(http://example.com/foo.png)" +
495+
"'>div content" +
496+
"</div></body></html>");
497+
String safeHtml = policy.sanitize(unsafeHtml);
498+
String expected =
499+
"<div style=\""
500+
+ "color:red;background-image:"
501+
+ "url(&#39;http://example.com/foo.png&#39;)"
502+
+ "\">div content</div>";
503+
assertEquals(expected, safeHtml);
504+
}
505+
506+
@Test
507+
public static final void testBackgroundImageWithImageFunction() {
508+
PolicyFactory policy = new HtmlPolicyBuilder()
509+
.allowStandardUrlProtocols()
510+
.allowStyling()
511+
.allowUrlsInStyles(AttributePolicy.IDENTITY_ATTRIBUTE_POLICY)
512+
.allowElements("div")
513+
.toFactory();
514+
String unsafeHtml = policy.sanitize(
515+
"<html><head><title>test</title></head><body>" +
516+
"<div style='" +
517+
"color: red; background-image: " +
518+
"image(\"blue sky.png\", blue)'>" +
519+
"div content" +
520+
"</div></body></html>");
521+
String safeHtml = policy.sanitize(unsafeHtml);
522+
String expected =
523+
"<div style=\""
524+
+ "color:red;background-image:"
525+
+ "image( url(&#39;blue%20sky.png&#39;) , blue )"
526+
+ "\">div content</div>";
527+
assertEquals(expected, safeHtml);
528+
}
529+
530+
@Test
531+
public static final void testBackgroundWithUrls() {
532+
HtmlPolicyBuilder builder = new HtmlPolicyBuilder()
533+
.allowStandardUrlProtocols()
534+
.allowStyling()
535+
.allowElements("div");
536+
537+
PolicyFactory noUrlsPolicy = builder.toFactory();
538+
PolicyFactory urlsPolicy = builder
539+
.allowUrlsInStyles(AttributePolicy.IDENTITY_ATTRIBUTE_POLICY)
540+
.toFactory();
541+
542+
String unsafeHtml =
543+
"<div style=\"background:&quot;//evil.org/foo.png&quot;\"></div>";
544+
545+
String safeWithUrls =
546+
"<div style=\"background:url(&#39;//evil.org/foo.png&#39;)\"></div>";
547+
String safeWithoutUrls = "<div></div>";
548+
549+
assertEquals(safeWithoutUrls, noUrlsPolicy.sanitize(unsafeHtml));
550+
assertEquals(safeWithUrls, urlsPolicy.sanitize(unsafeHtml));
551+
}
552+
553+
@Test
554+
public static final void testBackgroundsThatViolateGlobalUrlPolicy() {
555+
PolicyFactory policy = new HtmlPolicyBuilder()
556+
.allowStandardUrlProtocols()
557+
.allowStyling()
558+
.allowElements("div")
559+
.allowUrlsInStyles(AttributePolicy.IDENTITY_ATTRIBUTE_POLICY)
560+
.toFactory();
561+
562+
String unsafeHtml =
563+
"<div style=\"background:'javascript:alert(1337)'\"></div>";
564+
String safeHtml = "<div></div>";
565+
566+
assertEquals(safeHtml, policy.sanitize(unsafeHtml));
567+
568+
}
569+
570+
482571

483572
private static String apply(HtmlPolicyBuilder b) {
484573
return apply(b, EXAMPLE);

src/test/java/org/owasp/html/SanitizersTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ public static final void testAndOrdering() {
324324
+ "g"
325325
+ "<img src=\"http://example.org\"/>oogle</a>";
326326
String want = ""
327-
+ "xss<a href=\"http://www.google.de\" style=\"color:red\""
327+
+ "xss<a href=\"http://www.google.de\" style=\"color:red;\""
328328
+ " rel=\"nofollow\">"
329329
+ "g"
330330
+ "<img src=\"http://example.org\" />oogle</a>";

src/test/java/org/owasp/html/StylingPolicyTest.java

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
import org.junit.Test;
3434

35+
import com.google.common.base.Function;
36+
3537
import junit.framework.TestCase;
3638

3739
@SuppressWarnings("javadoc")
@@ -65,8 +67,10 @@ public static final void testColors() {
6567
assertSanitizedCss(null, "color: 000");
6668
assertSanitizedCss(null, "background-color: 000");
6769
// Not colors.
68-
assertSanitizedCss(null, "background: \"pwned.jpg\"");
69-
assertSanitizedCss(null, "background: url(pwned.jpg)");
70+
assertSanitizedCss(
71+
"background:url('pic.jpg#sanitized')", "background: \"pic.jpg\"");
72+
assertSanitizedCss(
73+
"background:url('pic.jpg#sanitized')", "background: url(pic.jpg)");
7074
assertSanitizedCss(null, "color:#urlabc");
7175
assertSanitizedCss(null, "color:#urlabcd");
7276
}
@@ -211,7 +215,7 @@ public static final void testBoxProperties() {
211215
}
212216

213217
@Test
214-
public static final void testUrls() {
218+
public static final void testLongUrls() {
215219
// Test that a long URL does not blow out the stack or consume quadratic
216220
// amounts of processor as when the CSS lexer was implemented as a bunch of
217221
// regular expressions.
@@ -291,9 +295,42 @@ public static final void testUrls() {
291295
assertSanitizedCss(null, longUrl);
292296
}
293297

298+
@Test
299+
public static final void testUrls() {
300+
assertSanitizedCss(
301+
"background-image:url('foo.gif#sanitized')",
302+
"background-image: \"foo.gif\"");
303+
assertSanitizedCss(
304+
"background-image:url('foo.gif#sanitized')",
305+
"background-image: 'foo.gif'");
306+
assertSanitizedCss(
307+
"background-image:url('foo.gif#sanitized')",
308+
"background-image:url(foo.gif)");
309+
assertSanitizedCss(
310+
"background-image:url('foo.gif#sanitized')",
311+
"background-image : url( foo.gif )");
312+
assertSanitizedCss(
313+
"background-image:url('foo.gif#sanitized')",
314+
"background-image: url('foo.gif')");
315+
assertSanitizedCss(
316+
"background-image:url('foo.gif#sanitized')",
317+
"background-image: URL( \"foo.gif\" )");
318+
}
319+
294320
private static void assertSanitizedCss(
295321
@Nullable String expectedCss, String css) {
296-
StylingPolicy stylingPolicy = new StylingPolicy(CssSchema.DEFAULT);
322+
StylingPolicy stylingPolicy = new StylingPolicy(
323+
CssSchema.DEFAULT,
324+
new Function<String, String>() {
325+
public String apply(String url) {
326+
String safeUrl =
327+
StandardUrlAttributePolicy.INSTANCE.apply("img", "src", url);
328+
if (safeUrl != null) {
329+
return safeUrl + "#sanitized";
330+
}
331+
return null;
332+
}
333+
});
297334
assertEquals(expectedCss, stylingPolicy.sanitizeCssProperties(css));
298335
}
299336
}

0 commit comments

Comments
 (0)