Java tutorial
/* * 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); } }