You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by jo...@apache.org on 2010/03/08 23:54:28 UTC

svn commit: r920557 - in /shindig/trunk/java/gadgets/src: main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java

Author: johnh
Date: Mon Mar  8 22:54:28 2010
New Revision: 920557

URL: http://svn.apache.org/viewvc?rev=920557&view=rev
Log:
Reintroduces CajaCssSanitizer in "root" caja directory, using ProxyUriManager instances rather than LinkRewriters.


Added:
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java?rev=920557&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java Mon Mar  8 22:54:28 2010
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import com.google.caja.lang.css.CssSchema;
+import com.google.caja.parser.AbstractParseTreeNode;
+import com.google.caja.parser.AncestorChain;
+import com.google.caja.parser.Visitor;
+import com.google.caja.parser.css.CssTree;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Sanitize a CSS tree using Caja. Strip properties and functions that represent
+ * ways to execute script. Specifically
+ *
+ * - Use Caja's CSS property whitelist
+ * - Use Caja's CSS function whitelist
+ * - Force @import through the proxy and require sanitization. If they cant be parsed, remove them
+ * - Force @url references to have the HTTP/HTTPS protocol
+ */
+public class CajaCssSanitizer {
+
+  private static final Logger logger = Logger.getLogger(CajaCssSanitizer.class.getName());
+
+  private static final Set<String> ALLOWED_URI_SCHEMES = ImmutableSet.of("http", "https");
+
+  private final CajaCssParser parser;
+
+  private final CssSchema schema;
+
+  @Inject
+  public CajaCssSanitizer(CajaCssParser parser) {
+    this.parser = parser;
+    schema = CssSchema.getDefaultCss21Schema(new SimpleMessageQueue());
+  }
+
+  /**
+   * Sanitize the CSS content of a style tag.
+   * @param content to sanitize
+   * @param linkContext url of containing content
+   * @param importRewriter to rewrite @imports to sanitizing proxy
+   * @param importRewriter to rewrite images to sanitizing proxy
+   */
+  public String sanitize(String content, Uri linkContext, ProxyUriManager importRewriter,
+      ProxyUriManager imageRewriter) {
+    try {
+      CssTree.StyleSheet stylesheet = parser.parseDom(content);
+      sanitize(stylesheet, linkContext, importRewriter, imageRewriter);
+      // Write the rewritten CSS back into the element
+      return parser.serialize(stylesheet);
+    } catch (GadgetException ge) {
+      // Failed to parse stylesheet so log and continue
+      logger.log(Level.INFO, "Failed to parse stylesheet", ge);
+      return "";
+    }
+  }
+
+  /**
+   * Sanitize the CSS content of a style tag.
+   * @param styleElem to sanitize
+   * @param linkContext url of containing content
+   * @param importRewriter to rewrite @imports to sanitizing proxy
+   * @param importRewriter to rewrite images to sanitizing proxy
+   */
+  public void sanitize(Element styleElem, Uri linkContext, ProxyUriManager importRewriter,
+      ProxyUriManager imageRewriter) {
+    String content = null;
+    try {
+      CssTree.StyleSheet stylesheet = parser.parseDom(styleElem.getTextContent());
+      sanitize(stylesheet, linkContext, importRewriter, imageRewriter);
+      // Write the rewritten CSS back into the element
+      content = parser.serialize(stylesheet);
+    } catch (GadgetException ge) {
+      // Failed to parse stylesheet so log and continue
+      logger.log(Level.INFO, "Failed to parse stylesheet", ge);
+    }
+    if (StringUtils.isEmpty(content)) {
+      // Remove the owning node
+      styleElem.getParentNode().removeChild(styleElem);
+    } else {
+      styleElem.setTextContent(content);
+    }
+  }
+
+  /**
+   * Sanitize the given CSS tree in-place by removing all non-whitelisted function calls
+   * @param css DOM root
+   * @param linkContext url of containing content
+   * @param importRewriter to rewrite links to sanitizing proxy
+   * @param imageRewriter to rewrite links to the sanitizing proxy
+   */
+  public void sanitize(CssTree css, final Uri linkContext, final ProxyUriManager importRewriter,
+      final ProxyUriManager imageRewriter) {
+    css.acceptPreOrder(new Visitor() {
+      public boolean visit(AncestorChain<?> ancestorChain) {
+        if (ancestorChain.node instanceof CssTree.Property) {
+          if (!schema.isPropertyAllowed(((CssTree.Property) ancestorChain.node).
+              getPropertyName())) {
+            // Remove offending property
+            if (logger.isLoggable(Level.FINE)) {
+              logger.log(Level.FINE, "Removing property "
+                  + ((CssTree.Property) ancestorChain.node).getPropertyName());
+            }
+            clean(ancestorChain);
+          }
+        } else if (ancestorChain.node instanceof CssTree.FunctionCall) {
+          if (!schema.isFunctionAllowed(((CssTree.FunctionCall)ancestorChain.node).getName())) {
+            // Remove offending node
+            if (logger.isLoggable(Level.FINE)) {
+              logger.log(Level.FINE, "Removing function "
+                  + ((CssTree.FunctionCall) ancestorChain.node).getName());
+            }
+            clean(ancestorChain);
+          }
+        } else if (ancestorChain.node instanceof CssTree.UriLiteral &&
+            !(ancestorChain.getParentNode() instanceof CssTree.Import)) {
+          String uri = ((CssTree.UriLiteral)ancestorChain.node).getValue();
+          if (isValidUri(uri)) {
+            // Assume the URI is for an image. Rewrite it using the image link rewriter
+            ((CssTree.UriLiteral)ancestorChain.node).setValue(
+                rewriteUri(imageRewriter, uri, linkContext));
+          } else {
+            // Remove offending node
+            if (logger.isLoggable(Level.FINE)) {
+              logger.log(Level.FINE, "Removing invalid URI " + uri);
+            }
+            clean(ancestorChain);
+          }
+        } else if (ancestorChain.node instanceof CssTree.Import) {
+          CssTree.Import importDecl = (CssTree.Import) ancestorChain.node;
+          String uri = importDecl.getUri().getValue();
+          if (isValidUri(uri)) {
+            importDecl.getUri().setValue(rewriteUri(importRewriter, uri, linkContext));
+          } else {
+            if (logger.isLoggable(Level.FINE)) {
+              logger.log(Level.FINE, "Removing invalid URI " + uri);
+            }
+            clean(ancestorChain);
+          }
+        }
+        return true;
+      }
+    }, null);
+  }
+  
+  private static String rewriteUri(ProxyUriManager proxyUriManager, String input, Uri context) {
+    Uri inboundUri = null;
+    try {
+      inboundUri = Uri.parse(input);
+    } catch (IllegalArgumentException e) {
+      // Don't rewrite at all.
+      return input;
+    }
+    if (context != null) {
+      inboundUri = context.resolve(inboundUri);
+    }
+    List<ProxyUriManager.ProxyUri> uris = ImmutableList.of(
+        new ProxyUriManager.ProxyUri(DomWalker.makeGadget(context), inboundUri));
+    List<Uri> rewritten = proxyUriManager.make(uris, null);
+    return rewritten.get(0).toString();
+  }
+
+  private boolean isValidUri(String uri) {
+    try {
+      String scheme = Uri.parse(uri).getScheme();
+      return StringUtils.isEmpty(scheme) ||
+          ALLOWED_URI_SCHEMES.contains(scheme.toLowerCase());
+    } catch (RuntimeException re) {
+      if (logger.isLoggable(Level.FINE)) {
+        logger.log(Level.FINE, "Failed to parse URI in CSS " + uri, re);
+      }
+    }
+    return false;
+  }
+
+  /**
+   * recurse up through chain to find a safe clean point
+   * @param chain chain of nodes
+   */
+  private static void clean(AncestorChain<?> chain) {
+    if (chain.node instanceof CssTree.Declaration ||
+        chain.node instanceof CssTree.Import) {
+      if (chain.getParentNode() instanceof CssTree.UserAgentHack) {
+        clean(chain.parent);
+      } else {
+        // Remove the entire subtree
+        ((AbstractParseTreeNode)chain.getParentNode()).removeChild(chain.node);
+      }
+    } else {
+      clean(chain.parent);
+    }
+  }
+}

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java?rev=920557&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java Mon Mar  8 22:54:28 2010
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.render.SanitizingProxyUriManager;
+import org.apache.shindig.gadgets.uri.PassthruManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import com.google.caja.parser.css.CssTree;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class CajaCssSanitizerTest extends EasyMockTestCase {
+
+  private CajaCssParser parser;
+  private CajaCssSanitizer sanitizer;
+  private final Uri DUMMY = Uri.parse("http://www.example.org/base");
+  private ProxyUriManager passthruManager;
+  private SanitizingProxyUriManager importRewriter;
+  private SanitizingProxyUriManager imageRewriter;
+
+  @Before
+  public void setUp() throws Exception {
+    parser = new CajaCssParser();
+    sanitizer = new CajaCssSanitizer(parser);
+    passthruManager = new PassthruManager("test.com", "/proxy/path");
+    importRewriter = new SanitizingProxyUriManager(passthruManager, "text/css");
+    imageRewriter = new SanitizingProxyUriManager(passthruManager, "image/*");
+  }
+
+  @Test
+  public void testPreserveSafe() throws Exception {
+    String css = ".xyz { font: bold;} A { color: #7f7f7f}";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, importRewriter, imageRewriter);
+    assertStyleEquals(css, styleSheet);
+  }
+
+  @Test
+  public void testSanitizeFunctionCall() throws Exception {
+    String css = ".xyz { font : iamevil(bold); }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz {}", styleSheet);
+  }
+
+  @Test
+   public void testSanitizeUnsafeProperties() throws Exception {
+    String css = ".xyz { behavior: url('xyz.htc'); -moz-binding:url(\"http://ha.ckers.org/xssmoz.xml#xss\") }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz {}", styleSheet);
+  }
+
+  @Test
+  public void testSanitizeScriptUrls() throws Exception {
+    String css = ".xyz { background: url('javascript:doevill'); background : url(vbscript:moreevil); }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz {}", styleSheet);
+  }
+
+  @Test
+  public void testProxyUrls() throws Exception {
+    String css = ".xyz { background: url('http://www.example.org/img.gif');}";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, importRewriter, imageRewriter);
+    assertStyleEquals(
+        ".xyz { background: url('http://test.com/proxy/path?url=" +
+        "http%3A%2F%2Fwww.example.org%2Fimg.gif" +
+        "&sanitize=1&rewriteMime=image%2F%2A');}", styleSheet);
+  }
+
+  @Test
+  public void testUrlEscaping() throws Exception {
+    String css = ".xyz { background: url('http://www.example.org/img.gif');}";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, importRewriter, imageRewriter);
+    assertEquals(".xyz{background:url('http://test.com/proxy/path?url=http%3A%2F%2Fwww.example.org%2F" +
+        "img.gif&sanitize=1&rewriteMime=image%2F%2A');}",
+        parser.serialize(styleSheet).replaceAll("\\s", ""));
+  }
+
+  public void assertStyleEquals(String expected, CssTree.StyleSheet styleSheet) throws Exception {
+    assertEquals(parser.serialize(parser.parseDom(expected)), parser.serialize(styleSheet));
+  }
+}