org.zaproxy.zap.extension.pscanrules.ContentSecurityPolicyScanner.java Source code

Java tutorial

Introduction

Here is the source code for org.zaproxy.zap.extension.pscanrules.ContentSecurityPolicyScanner.java

Source

/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2016 The ZAP Development Team
 *
 * Licensed 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.zaproxy.zap.extension.pscanrules;

import com.shapesecurity.salvation.ParserWithLocation;
import com.shapesecurity.salvation.data.Notice;
import com.shapesecurity.salvation.data.Origin;
import com.shapesecurity.salvation.data.Policy;
import com.shapesecurity.salvation.data.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.htmlparser.jericho.Source;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.core.scanner.Alert;
import org.parosproxy.paros.core.scanner.Plugin.AlertThreshold;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.zap.extension.pscan.PassiveScanThread;
import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;

/**
 * Content Security Policy Header passive scan rule https://github.com/zaproxy/zaproxy/issues/527
 * Meant to complement the CSP Header Missing passive scan rule
 *
 * <p>TODO: Add handling for multiple CSP headers TODO: Add handling for CSP via META tag See
 * https://github.com/shapesecurity/salvation/issues/149 for info on combining CSP policies
 *
 * @author kingthorin+owaspzap@gmail.com
 */
public class ContentSecurityPolicyScanner extends PluginPassiveScanner {

    private static final String MESSAGE_PREFIX = "pscanrules.cspscanner.";
    private static final int PLUGIN_ID = 10055;
    private static final Logger LOGGER = Logger.getLogger(ContentSecurityPolicyScanner.class);

    private static final String HTTP_HEADER_CSP = "Content-Security-Policy";
    private static final String HTTP_HEADER_XCSP = "X-Content-Security-Policy";
    private static final String HTTP_HEADER_WEBKIT_CSP = "X-WebKit-CSP";

    private static final String WILDCARD_URI = "http://*";
    private static final URI PARSED_WILDCARD_URI = URI.parse(WILDCARD_URI);

    private PassiveScanThread parent = null;

    @Override
    public void setParent(PassiveScanThread parent) {
        this.parent = parent;
    }

    @Override
    public void scanHttpRequestSend(HttpMessage msg, int id) {
        // Only checking the response for this plugin
    }

    @Override
    public void scanHttpResponseReceive(HttpMessage msg, int id, Source source) {
        boolean cspHeaderFound = false;
        int noticesRisk = Alert.RISK_INFO;
        // LOGGER.setLevel(Level.DEBUG); //Enable for debugging

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Start " + id + " : " + msg.getRequestHeader().getURI().toString());
        }

        long start = System.currentTimeMillis();

        if (!msg.getResponseHeader().isHtml() && !AlertThreshold.LOW.equals(this.getAlertThreshold())) {
            // Only really applies to HTML responses, but also check everything on Low threshold
            return;
        }

        // Content-Security-Policy is supported by Chrome 25+, Firefox 23+,
        // Safari 7+, Edge but not Internet Explorer
        Vector<String> cspOptions = msg.getResponseHeader().getHeaders(HTTP_HEADER_CSP);
        if (cspOptions != null && !cspOptions.isEmpty()) {
            cspHeaderFound = true;
        }

        // X-Content-Security-Policy is an older header, supported by Firefox
        // 4.0+, and IE 10+ (in a limited fashion)
        Vector<String> xcspOptions = msg.getResponseHeader().getHeaders(HTTP_HEADER_XCSP);
        if (xcspOptions != null && !xcspOptions.isEmpty()) {
            raiseAlert(msg, Constant.messages.getString(MESSAGE_PREFIX + "xcsp.name"), id,
                    Constant.messages.getString(MESSAGE_PREFIX + "xcsp.desc"),
                    getHeaderField(msg, HTTP_HEADER_XCSP).get(0), cspHeaderFound ? Alert.RISK_INFO : Alert.RISK_LOW,
                    xcspOptions.get(0));
        }

        // X-WebKit-CSP is supported by Chrome 14+, and Safari 6+
        Vector<String> xwkcspOptions = msg.getResponseHeader().getHeaders(HTTP_HEADER_WEBKIT_CSP);
        if (xwkcspOptions != null && !xwkcspOptions.isEmpty()) {
            raiseAlert(msg, Constant.messages.getString(MESSAGE_PREFIX + "xwkcsp.name"), id,
                    Constant.messages.getString(MESSAGE_PREFIX + "xwkcsp.desc"),
                    getHeaderField(msg, HTTP_HEADER_WEBKIT_CSP).get(0),
                    cspHeaderFound ? Alert.RISK_INFO : Alert.RISK_LOW, xwkcspOptions.get(0));
        }

        if (cspHeaderFound) {
            ArrayList<Notice> notices = new ArrayList<>();
            Origin origin = URI.parse(msg.getRequestHeader().getURI().toString());
            String policyText = cspOptions.toString().replace("[", "").replace("]", "");
            Policy pol = ParserWithLocation.parse(policyText, origin, notices); // Populate notices

            if (!notices.isEmpty()) {
                String cspNoticesString = getCSPNoticesString(notices);
                if (cspNoticesString.contains(Constant.messages.getString(MESSAGE_PREFIX + "notices.errors"))
                        || cspNoticesString
                                .contains(Constant.messages.getString(MESSAGE_PREFIX + "notices.warnings"))) {
                    noticesRisk = Alert.RISK_LOW;
                } else {
                    noticesRisk = Alert.RISK_INFO;
                }
                raiseAlert(msg, Constant.messages.getString(MESSAGE_PREFIX + "notices.name"), id, cspNoticesString,
                        getHeaderField(msg, HTTP_HEADER_CSP).get(0), noticesRisk, cspOptions.get(0));
            }

            List<String> allowedWildcardSources = getAllowedWildcardSources(policyText, origin);
            if (!allowedWildcardSources.isEmpty()) {
                String allowedWildcardSrcs = allowedWildcardSources.toString().replace("[", "").replace("]", "");
                String wildcardSrcDesc = Constant.messages.getString(MESSAGE_PREFIX + "wildcard.desc",
                        allowedWildcardSrcs);
                raiseAlert(msg, Constant.messages.getString(MESSAGE_PREFIX + "wildcard.name"), id, wildcardSrcDesc,
                        getHeaderField(msg, HTTP_HEADER_CSP).get(0), Alert.RISK_MEDIUM, cspOptions.get(0));
            }

            if (pol.allowsUnsafeInlineScript()) {
                raiseAlert(msg, Constant.messages.getString(MESSAGE_PREFIX + "scriptsrc.unsafe.name"), id,
                        Constant.messages.getString(MESSAGE_PREFIX + "scriptsrc.unsafe.desc"),
                        getHeaderField(msg, HTTP_HEADER_CSP).get(0), Alert.RISK_MEDIUM, cspOptions.get(0));
            }

            if (pol.allowsUnsafeInlineStyle()) {
                raiseAlert(msg, Constant.messages.getString(MESSAGE_PREFIX + "stylesrc.unsafe.name"), id,
                        Constant.messages.getString(MESSAGE_PREFIX + "stylesrc.unsafe.desc"),
                        getHeaderField(msg, HTTP_HEADER_CSP).get(0), Alert.RISK_MEDIUM, cspOptions.get(0));
            }
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("\tScan of record " + String.valueOf(id) + " took " + (System.currentTimeMillis() - start)
                    + " ms");
        }
    }

    private String getCSPNoticesString(ArrayList<Notice> notices) {
        char NEWLINE = '\n';
        StringBuilder returnSb = new StringBuilder();

        ArrayList<Notice> errorsList = Notice.getAllErrors(notices);
        if (!errorsList.isEmpty()) {
            returnSb.append(Constant.messages.getString(MESSAGE_PREFIX + "notices.errors")).append(NEWLINE);
            for (Notice notice : errorsList) {
                returnSb.append(notice.show()).append(NEWLINE);
                // Ex: 1:1: Unrecognised directive-name: "image-src".
            }
        }

        ArrayList<Notice> warnList = Notice.getAllWarnings(notices);
        if (!warnList.isEmpty()) {
            returnSb.append(Constant.messages.getString(MESSAGE_PREFIX + "notices.warnings")).append(NEWLINE);
            for (Notice notice : warnList) {
                returnSb.append(notice.show()).append(NEWLINE);
                // Ex: 1:25: This host name is unusual, and likely meant to be a
                // keyword that is missing the required quotes: 'none'.
            }
        }

        ArrayList<Notice> infoList = Notice.getAllInfos(notices);
        if (!infoList.isEmpty()) {
            returnSb.append(Constant.messages.getString(MESSAGE_PREFIX + "notices.infoitems")).append(NEWLINE);
            for (Notice notice : infoList) {
                returnSb.append(notice.show()).append(NEWLINE);
                // Ex: 1:31: A draft of the next version of CSP deprecates
                // report-uri in favour of a new report-to directive.
            }
        }
        return returnSb.toString();
    }

    /**
     * Extracts a list of headers, and returns them without changing their cases.
     *
     * @param msg HTTP Response message
     * @param header The header field(s) to be found
     * @return list of the matched headers
     */
    private List<String> getHeaderField(HttpMessage msg, String header) {
        List<String> matchedHeaders = new ArrayList<>();
        String headers = msg.getResponseHeader().toString();
        String[] headerElements = headers.split("\\r\\n");
        Pattern pattern = Pattern.compile("^" + header, Pattern.CASE_INSENSITIVE);
        for (String hdr : headerElements) {
            Matcher matcher = pattern.matcher(hdr);
            if (matcher.find()) {
                String match = matcher.group();
                matchedHeaders.add(match);
            }
        }
        return matchedHeaders;
    }

    private List<String> getAllowedWildcardSources(String policyText, Origin origin) {

        List<String> allowedSources = new ArrayList<String>();
        Policy pol = ParserWithLocation.parse(policyText, origin);

        if (pol.allowsScriptFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("script-src");
            allowedSources.add("script-src-elem");
            allowedSources.add("script-src-attr");
        }
        if (pol.allowsStyleFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("style-src");
            allowedSources.add("style-src-elem");
            allowedSources.add("style-src-attr");
        }
        if (pol.allowsImgFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("img-src");
        }
        if (pol.allowsConnectTo(PARSED_WILDCARD_URI)) {
            allowedSources.add("connect-src");
        }
        if (pol.allowsFrameFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("frame-src");
        }
        if (pol.allowsFrameAncestor(PARSED_WILDCARD_URI)) {
            allowedSources.add("frame-ancestor");
        }
        if (pol.allowsFontFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("font-src");
        }
        if (pol.allowsMediaFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("media-src");
        }
        if (pol.allowsObjectFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("object-src");
        }
        if (pol.allowsManifestFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("manifest-src");
        }
        if (pol.allowsWorkerFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("worker-src");
        }
        if (pol.allowsPrefetchFromSource(PARSED_WILDCARD_URI)) {
            allowedSources.add("prefetch-src");
        }
        return allowedSources;
    }

    @Override
    public int getPluginId() {
        return PLUGIN_ID;
    }

    @Override
    public String getName() {
        return Constant.messages.getString(MESSAGE_PREFIX + "name");
    }

    private String getSolution() {
        return Constant.messages.getString(MESSAGE_PREFIX + "soln");
    }

    private String getReference() {
        return Constant.messages.getString(MESSAGE_PREFIX + "refs");
    }

    private void raiseAlert(HttpMessage msg, String name, int id, String description, String param, int risk,
            String evidence) {
        String alertName = StringUtils.isEmpty(name) ? getName() : getName() + ": " + name;

        Alert alert = new Alert(getPluginId(), risk, Alert.CONFIDENCE_MEDIUM, // PluginID, Risk, Reliability
                alertName);
        alert.setDetail(description, // Description
                msg.getRequestHeader().getURI().toString(), // URI
                param, // Param
                "", // Attack
                "", // Other info
                getSolution(), // Solution
                getReference(), // References
                evidence, // Evidence
                16, // CWE-16: Configuration
                15, // WASC-15: Application Misconfiguration
                msg); // HttpMessage
        parent.raiseAlert(id, alert);
    }
}