org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer.java

Source

/*
 * 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.GadgetContext;
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 LOG = 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 gadgetContext The gadget context.
     * @param importRewriter to rewrite @imports to sanitizing proxy
     * @param imageRewriter to rewrite images to sanitizing proxy
     * @return Sanitized css.
     */
    public String sanitize(String content, Uri linkContext, GadgetContext gadgetContext,
            ProxyUriManager importRewriter, ProxyUriManager imageRewriter) {
        try {
            CssTree.StyleSheet stylesheet = parser.parseDom(content, linkContext);
            sanitize(stylesheet, linkContext, gadgetContext, 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
            LOG.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 gadgetContext The gadget context.
     * @param importRewriter to rewrite @imports to sanitizing proxy
     * @param imageRewriter to rewrite images to sanitizing proxy
     */
    public void sanitize(Element styleElem, Uri linkContext, GadgetContext gadgetContext,
            ProxyUriManager importRewriter, ProxyUriManager imageRewriter) {
        String content = null;
        try {
            CssTree.StyleSheet stylesheet = parser.parseDom(styleElem.getTextContent(), linkContext);
            sanitize(stylesheet, linkContext, gadgetContext, 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
            LOG.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 gadgetContext The gadget context.
     * @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 GadgetContext gadgetContext,
            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 (LOG.isLoggable(Level.FINE)) {
                            LOG.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 (LOG.isLoggable(Level.FINE)) {
                            LOG.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, gadgetContext));
                    } else {
                        // Remove offending node
                        if (LOG.isLoggable(Level.FINE)) {
                            LOG.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, gadgetContext));
                    } else {
                        if (LOG.isLoggable(Level.FINE)) {
                            LOG.log(Level.FINE, "Removing invalid URI " + uri);
                        }
                        clean(ancestorChain);
                    }
                }
                return true;
            }
        }, null);
    }

    private static String rewriteUri(ProxyUriManager proxyUriManager, String input, final Uri context,
            GadgetContext gadgetContext) {
        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(new GadgetContext(gadgetContext) {
                    @Override
                    public Uri getUrl() {
                        return 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 (LOG.isLoggable(Level.FINE)) {
                LOG.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);
        }
    }
}