org.zaproxy.zap.extension.pscanrulesAlpha.StrictTransportSecurityScanner.java Source code

Java tutorial

Introduction

Here is the source code for org.zaproxy.zap.extension.pscanrulesAlpha.StrictTransportSecurityScanner.java

Source

/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2014 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.pscanrulesAlpha;

import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.htmlparser.jericho.Element;
import net.htmlparser.jericho.HTMLElementName;
import net.htmlparser.jericho.Source;
import org.apache.commons.httpclient.URI;
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.HttpHeader;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.network.HttpStatusCode;
import org.zaproxy.zap.extension.pscan.PassiveScanThread;
import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;

/**
 * Strict-Transport-Security Header Not Set passive scan rule
 * https://github.com/zaproxy/zaproxy/issues/1169
 *
 * @author kingthorin+owaspzap@gmail.com
 */
public class StrictTransportSecurityScanner extends PluginPassiveScanner {

    private static final String MESSAGE_PREFIX = "pscanalpha.stricttransportsecurity.";
    private static final int PLUGIN_ID = 10035;
    private static final String STS_HEADER = "Strict-Transport-Security";

    // max-age=0 disabled HSTS. It's allowed by the spec,
    // and is used to reset browser's settings for HSTS.
    // If found raise an alert.
    // Pattern accounts for potential spaces and quotes
    private static final Pattern BAD_MAX_AGE_PATT = Pattern.compile("\\bmax-age\\s*=\\s*\'*\"*\\s*0\\s*\"*\'*\\s*",
            Pattern.CASE_INSENSITIVE);
    // Ensure max-age actually contains a digit
    private static final Pattern MAX_AGE_PATT = Pattern
            .compile("\\bmax-age\\s*=\\s*\'*\"*\\s*\\s*\\d+\\s*\"*\'*\\s*", Pattern.CASE_INSENSITIVE);
    // Ensure quotes aren't before max-age
    private static final Pattern MALFORMED_MAX_AGE = Pattern.compile("[\'+|\"+]\\s*max", Pattern.CASE_INSENSITIVE);
    // Ensure printable ascii
    private static final Pattern WELL_FORMED_PATT = Pattern.compile("\\p{Print}*", Pattern.CASE_INSENSITIVE);

    private enum VulnType {
        HSTS_MISSING, HSTS_MAX_AGE_DISABLED, HSTS_MULTIPLE_HEADERS, HSTS_ON_PLAIN_RESP, HSTS_MAX_AGE_MISSING, HSTS_META, HSTS_MALFORMED_MAX_AGE, HSTS_MALFORMED_CONTENT
    };

    private PassiveScanThread parent = null;
    private static final Logger logger = Logger.getLogger(StrictTransportSecurityScanner.class);

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

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

    private void raiseAlert(VulnType currentVT, String evidence, HttpMessage msg, int id) {
        Alert alert = new Alert(getPluginId(), // PluginID
                getRisk(currentVT), Alert.CONFIDENCE_HIGH, // Reliability
                getAlertElement(currentVT, "name")); // Name
        alert.setDetail(getAlertElement(currentVT, "desc"), // Description
                msg.getRequestHeader().getURI().toString(), // URI
                "", // Param
                "", // Attack
                "", // Other info
                getAlertElement(currentVT, "soln"), // Solution
                getAlertElement(currentVT, "refs"), // References
                evidence, // Evidence
                16, // CWE-16: Configuration
                15, // WASC-15: Application Misconfiguration
                msg); // HttpMessage
        parent.raiseAlert(id, alert);
    }

    @Override
    public void scanHttpResponseReceive(HttpMessage msg, int id, Source source) {
        long start = System.currentTimeMillis();
        Vector<String> stsOption = msg.getResponseHeader().getHeaders(STS_HEADER);
        String metaHSTS = getMetaHSTSEvidence(source);

        if (msg.getRequestHeader().isSecure()) { // No point reporting missing for non-SSL resources
            // Content available via both HTTPS and HTTP is a separate though related issue
            if (stsOption == null) { // Header NOT found
                boolean report = true;
                if (!this.getAlertThreshold().equals(AlertThreshold.LOW)
                        && HttpStatusCode.isRedirection(msg.getResponseHeader().getStatusCode())) {
                    // Only report https redirects to the same domain at low threshold
                    try {
                        String redirStr = msg.getResponseHeader().getHeader(HttpHeader.LOCATION);
                        URI srcUri = msg.getRequestHeader().getURI();
                        URI redirUri = new URI(redirStr, false);
                        if (redirUri.isRelativeURI() || (redirUri.getScheme().equalsIgnoreCase("https")
                                && redirUri.getHost().equals(srcUri.getHost())
                                && redirUri.getPort() == srcUri.getPort())) {
                            report = false;
                        }
                    } catch (Exception e) {
                        // Ignore, so report the missing header
                    }
                }
                if (report) {
                    raiseAlert(VulnType.HSTS_MISSING, null, msg, id);
                }
            } else if (stsOption.size() > 1) { // More than one header found
                raiseAlert(VulnType.HSTS_MULTIPLE_HEADERS, null, msg, id);
            } else { // Single HSTS header entry
                String stsOptionString = stsOption.get(0);
                Matcher badAgeMatcher = BAD_MAX_AGE_PATT.matcher(stsOptionString);
                Matcher maxAgeMatcher = MAX_AGE_PATT.matcher(stsOptionString);
                Matcher malformedMaxAgeMatcher = MALFORMED_MAX_AGE.matcher(stsOptionString);
                Matcher wellformedMatcher = WELL_FORMED_PATT.matcher(stsOptionString);
                if (!wellformedMatcher.matches()) {
                    // Well formed pattern didn't match (perhaps curly quotes or some other unwanted
                    // character(s))
                    raiseAlert(VulnType.HSTS_MALFORMED_CONTENT, STS_HEADER, msg, id);
                } else if (badAgeMatcher.find()) {
                    // Matched BAD_MAX_AGE_PATT, max-age is zero
                    raiseAlert(VulnType.HSTS_MAX_AGE_DISABLED, badAgeMatcher.group(), msg, id);
                } else if (!maxAgeMatcher.find()) {
                    // Didn't find a digit value associated with max-age
                    raiseAlert(VulnType.HSTS_MAX_AGE_MISSING, stsOption.get(0), msg, id);
                } else if (malformedMaxAgeMatcher.find()) {
                    // Found max-age but it was malformed
                    raiseAlert(VulnType.HSTS_MALFORMED_MAX_AGE, stsOption.get(0), msg, id);
                }
            }
        } else if (AlertThreshold.LOW.equals(this.getAlertThreshold()) && stsOption != null
                && !stsOption.isEmpty()) {
            // isSecure is false at this point
            // HSTS Header found on non-HTTPS response (technically there could be more than one
            // but we only care that there is one or more)
            raiseAlert(VulnType.HSTS_ON_PLAIN_RESP, stsOption.get(0), msg, id);
        }

        if (metaHSTS != null) {
            // HSTS found defined by META tag
            raiseAlert(VulnType.HSTS_META, metaHSTS, msg, id);
        }

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

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

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

    private String getAlertElement(VulnType currentVT, String element) {
        String elementValue = "";
        switch (currentVT) {
        case HSTS_MISSING:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + element);
            break;
        case HSTS_MAX_AGE_DISABLED:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + "max.age." + element);
            break;
        case HSTS_MULTIPLE_HEADERS:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + "compliance.multiple.header." + element);
            break;
        case HSTS_ON_PLAIN_RESP:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + "plain.resp." + element);
            break;
        case HSTS_MAX_AGE_MISSING:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + "compliance.max.age.missing." + element);
            break;
        case HSTS_META:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + "compliance.meta." + element);
            break;
        case HSTS_MALFORMED_MAX_AGE:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + "compliance.max.age.malformed." + element);
            break;
        case HSTS_MALFORMED_CONTENT:
            elementValue = Constant.messages.getString(MESSAGE_PREFIX + "compliance.malformed.content." + element);
            break;
        }
        return elementValue;
    }

    private int getRisk(VulnType currentVT) {
        switch (currentVT) {
        case HSTS_MISSING:
        case HSTS_MAX_AGE_DISABLED:
        case HSTS_MULTIPLE_HEADERS:
        case HSTS_MAX_AGE_MISSING:
        case HSTS_META:
        case HSTS_MALFORMED_MAX_AGE:
        case HSTS_MALFORMED_CONTENT:
            return Alert.RISK_LOW;
        case HSTS_ON_PLAIN_RESP:
        default:
            return Alert.RISK_INFO;
        }
    }

    /**
     * Checks the source of the response for HSTS being set via a META tag which is explicitly not
     * supported per the spec (rfc6797).
     *
     * @param source the source of the response to be analyzed.
     * @return returns a string if HSTS was set via META (for use as alert evidence) otherwise
     *     return {@code null}.
     * @see <a href="https://tools.ietf.org/html/rfc6797#section-8.5">RFC 6797 Section 8.5</a>
     */
    private String getMetaHSTSEvidence(Source source) {
        List<Element> metaElements = source.getAllElements(HTMLElementName.META);
        String httpEquiv;

        if (metaElements != null) {
            for (Element metaElement : metaElements) {
                httpEquiv = metaElement.getAttributeValue("http-equiv");
                if (STS_HEADER.equalsIgnoreCase(httpEquiv)) {
                    return httpEquiv; // This is a META which attempts to define HSTS return it's
                    // value
                }
            }
        }
        return null;
    }
}