org.zaproxy.zap.extension.ascanrules.TestSQLInjection.java Source code

Java tutorial

Introduction

Here is the source code for org.zaproxy.zap.extension.ascanrules.TestSQLInjection.java

Source

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

import difflib.Delta;
import difflib.DiffUtils;
import difflib.Patch;
import java.io.IOException;
import java.net.SocketException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.InvalidRedirectLocationException;
import org.apache.commons.httpclient.URI;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.core.scanner.AbstractAppParamPlugin;
import org.parosproxy.paros.core.scanner.Alert;
import org.parosproxy.paros.core.scanner.Category;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.zap.extension.authentication.ExtensionAuthentication;
import org.zaproxy.zap.model.Context;
import org.zaproxy.zap.model.Tech;
import org.zaproxy.zap.model.TechSet;

/**
 * TODO: implement stacked query check, since it is actually supported on more RDBMS drivers /
 * frameworks than not (MySQL on PHP/ASP does not by default, but can). PostgreSQL and MSSQL on ASP,
 * ASP.NET, and PHP *do* support it, for instance. It's better to put the code here and try it for
 * all RDBMSs as a result. Use the following variables: doStackedBased, doStackedMaxRequests,
 * countStackedBasedRequests TODO: change the Alert Titles. TODO: implement mode checks
 * (Mode.standard, Mode.safe, Mode.protected) for 2.* using "implements SessionChangedListener"
 *
 * <p>The SQLInjection plugin identifies SQL Injection vulnerabilities. Note the ordering of checks,
 * for efficiency is : 1) Error based 2) Boolean Based 3) UNION based 4) Stacked (TODO: implement
 * stacked based) 5) Blind/Time Based (RDBMS specific, so not done here right now)
 *
 * @author 70pointer
 */
public class TestSQLInjection extends AbstractAppParamPlugin {

    /** Prefix for internationalised messages used by this rule */
    private static final String MESSAGE_PREFIX = "ascanrules.testsqlinjection.";

    /** Did SQLInjection get found yet? */
    private boolean sqlInjectionFoundForUrl = false;

    private String sqlInjectionAttack = null;
    private HttpMessage refreshedmessage = null;
    private String mResBodyNormalUnstripped = null;
    private String mResBodyNormalStripped = null;
    // what do we do at each attack strength?
    // (some SQL Injection vulns would be picked up by multiple types of checks, and we skip out
    // after the first alert for a URL)
    private boolean doSpecificErrorBased = false;
    private boolean doGenericErrorBased = false;
    private boolean doBooleanBased = false;
    private boolean doUnionBased = false;
    private boolean doExpressionBased = false;
    private boolean doOrderByBased = false;
    // private boolean doStackedBased = false;  //TODO: use in the stacked based implementation
    // how many requests can we fire for each method? will be set depending on the attack strength
    private int doErrorMaxRequests = 0;
    private int doBooleanMaxRequests = 0;
    private int doUnionMaxRequests = 0;
    private int doExpressionMaxRequests = 0;
    private int doOrderByMaxRequests = 0;
    // private int doStackedMaxRequests = 0;   //TODO: use in the stacked based implementation
    // how many requests have we fired up?
    private int countErrorBasedRequests = 0;
    private int countExpressionBasedRequests = 0;
    private int countBooleanBasedRequests = 0;
    private int countUnionBasedRequests = 0;
    private int countOrderByBasedRequests = 0;
    // private int countStackedBasedRequests = 0;  //TODO: use in the stacked based queries
    // implementation
    /**
     * generic one-line comment. Various RDBMS Documentation suggests that this syntax works with
     * almost every single RDBMS considered here
     */
    public static final String SQL_ONE_LINE_COMMENT = " -- ";
    /**
     * used to inject to check for SQL errors: some basic SQL metacharacters ordered so as to
     * maximise SQL errors Note that we do separate runs for each family of characters, in case one
     * family are filtered out, the others might still get past
     */
    private static final String[] SQL_CHECK_ERR = { "'", "\"", ";", ")", "(", "NULL", "'\"" };

    /**
     * A collection of RDBMS with its error message fragments and {@code Tech}.
     *
     * <p>The error messages are in order they should be checked to allow the more (subjectively
     * judged) common cases to be tested first.
     *
     * <p><strong>Note:</strong> the messages should represent actual (driver level) error messages
     * for things like syntax error, otherwise we are simply guessing that the string should/might
     * occur.
     *
     * @see Tech
     */
    private enum RDBMS {
        // TODO: add other specific UNION based error messages for Union here: PostgreSQL, Sybase,
        // DB2, Informix, etc

        // DONE: we have implemented a MySQL specific scanner. See SQLInjectionMySQL
        MySQL("MySQL", Tech.MySQL,
                Arrays.asList(new String[] { "\\Qcom.mysql.jdbc.exceptions\\E", "\\Qorg.gjt.mm.mysql\\E",
                        "\\QODBC driver does not support\\E",
                        "\\QThe used SELECT statements have a different number of columns\\E" }),
                Arrays.asList(
                        new String[] { "\\QThe used SELECT statements have a different number of columns\\E" })),

        // TODO: implement a plugin that uses Microsoft SQL specific functionality to detect SQL
        // Injection vulnerabilities
        MsSQL("Microsoft SQL Server", Tech.MsSQL, Arrays.asList(new String[] { "\\Qcom.microsoft.sqlserver.jdbc\\E",
                "\\Qcom.microsoft.jdbc\\E", "\\Qcom.inet.tds\\E", "\\Qcom.microsoft.sqlserver.jdbc\\E",
                "\\Qcom.ashna.jturbo\\E", "\\Qweblogic.jdbc.mssqlserver\\E", "\\Q[Microsoft]\\E",
                "\\Q[SQLServer]\\E", "\\Q[SQLServer 2000 Driver for JDBC]\\E", "\\Qnet.sourceforge.jtds.jdbc\\E", // see also be Sybase. could be
                // either!
                "\\Q80040e14\\E", "\\Q800a0bcd\\E", "\\Q80040e57\\E", "\\QODBC driver does not support\\E",
                "\\QAll queries in an SQL statement containing a UNION operator must have an equal number of expressions in their target lists\\E",
                "\\QAll queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists\\E" }),
                Arrays.asList(new String[] {
                        "\\QAll queries in an SQL statement containing a UNION operator must have an equal number of expressions in their target lists\\E",
                        "\\QAll queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists\\E" })),

        // DONE: we have implemented an Oracle specific scanner. See SQLInjectionOracle
        Oracle("Oracle", Tech.Oracle,
                Arrays.asList(new String[] { "\\Qoracle.jdbc\\E", "\\QSQLSTATE[HY\\E", "\\QORA-00933\\E",
                        "\\QORA-06512\\E", // indicates the line number of an error
                        "\\QSQL command not properly ended\\E", "\\QORA-00942\\E", // table or view does not exist
                        "\\QORA-29257\\E", // host unknown
                        "\\QORA-00932\\E", // inconsistent datatypes
                        "\\Qquery block has incorrect number of result columns\\E", "\\QORA-01789\\E" }),
                Arrays.asList(new String[] { "\\Qquery block has incorrect number of result columns\\E",
                        "\\QORA-01789\\E" })),

        // TODO: implement a plugin that uses DB2 specific functionality to detect SQL Injection
        // vulnerabilities
        DB2("IBM DB2", Tech.Db2, "\\Qcom.ibm.db2.jcc\\E", "\\QCOM.ibm.db2.jdbc\\E"),

        // DONE: we have implemented a PostgreSQL specific scanner. See SQLInjectionPostgresql
        PostgreSQL("PostgreSQL", Tech.PostgreSQL,
                Arrays.asList(new String[] { "\\Qorg.postgresql.util.PSQLException\\E", "\\Qorg.postgresql\\E",
                        "\\Qeach UNION query must have the same number of columns\\E" }),
                Arrays.asList(new String[] { "\\Qeach UNION query must have the same number of columns\\E" })),

        // TODO: implement a plugin that uses Sybase specific functionality to detect SQL Injection
        // vulnerabilities
        // Note: this plugin would also detect Microsoft SQL Server vulnerabilities, due to common
        // syntax.
        Sybase("Sybase", Tech.Sybase, "\\Qcom.sybase.jdbc\\E", "\\Qcom.sybase.jdbc2.jdbc\\E",
                "\\Qcom.sybase.jdbc3.jdbc\\E", "\\Qnet.sourceforge.jtds.jdbc\\E" // see also Microsoft SQL Server. could be either!
        ),

        // TODO: implement a plugin that uses Informix specific functionality to detect SQL
        // Injection vulnerabilities
        Informix("Informix", Tech.Db, "\\Qcom.informix.jdbc\\E"),

        // TODO: implement a plugin that uses Firebird specific functionality to detect SQL
        // Injection vulnerabilities
        Firebird("Firebird", Tech.Firebird, "\\Qorg.firebirdsql.jdbc\\E"),

        // TODO: implement a plugin that uses IDS Server specific functionality to detect SQL
        // Injection vulnerabilities
        IdsServer("IDS Server", Tech.Db, "\\Qids.sql\\E"),

        // TODO: implement a plugin that uses InstantDB specific functionality to detect SQL
        // Injection vulnerabilities
        InstantDB("InstantDB", Tech.Db, "\\Qorg.enhydra.instantdb.jdbc\\E", "\\Qjdbc.idb\\E"),

        // TODO: implement a plugin that uses Interbase specific functionality to detect SQL
        // Injection vulnerabilities
        Interbase("Interbase", Tech.Db, "\\Qinterbase.interclient\\E"),

        // DONE: we have implemented a Hypersonic specific scanner. See SQLInjectionHypersonic
        HypersonicSQL("Hypersonic SQL", Tech.HypersonicSQL, Arrays.asList(new String[] { "\\Qorg.hsql\\E",
                "\\QhSql.\\E", "\\QUnexpected token , requires FROM in statement\\E",
                "\\QUnexpected end of command in statement\\E", "\\QColumn count does not match in statement\\E", // TODO: too generic
                // to leave in???
                "\\QTable not found in statement\\E", // TODO: too generic to leave
                // in???
                "\\QUnexpected token:\\E" // TODO: too generic to leave in??? Works very
        // nicely in
        // Hypersonic cases, however
        }), Arrays.asList(new String[] { "\\QUnexpected end of command in statement\\E", // needs a table name in
                // a UNION query. Like
                // Oracle?
                "\\QColumn count does not match in statement\\E" })),

        // TODO: implement a plugin that uses Sybase SQL Anywhere specific functionality to detect
        // SQL Injection vulnerabilities
        SybaseSQL("Sybase SQL Anywhere", Tech.Sybase, "\\Qsybase.jdbc.sqlanywhere\\E"),

        // TODO: implement a plugin that uses PointBase specific functionality to detect SQL
        // Injection vulnerabilities
        Pointbase("Pointbase", Tech.Db, "\\Qcom.pointbase.jdbc\\E"),

        // TODO: implement a plugin that uses Cloudbase specific functionality to detect SQL
        // Injection vulnerabilities
        Cloudscape("Cloudscape", Tech.Db, "\\Qdb2j.\\E", "\\QCOM.cloudscape\\E", "\\QRmiJdbc.RJDriver\\E"),

        // TODO: implement a plugin that uses Ingres specific functionality to detect SQL Injection
        // vulnerabilities
        Ingres("Ingres", Tech.Db, "\\Qcom.ingres.jdbc\\E"),

        SQLite("SQLite", Tech.SQLite, Arrays.asList(new String[] { "near \".+\": syntax error", // uses a regular expression..
                "\\QSELECTs to the left and right of UNION do not have the same number of result columns\\E" }),
                Arrays.asList(new String[] {
                        "\\QSELECTs to the left and right of UNION do not have the same number of result columns\\E" })),

        // generic error message fragments that do not fingerprint the RDBMS, but that may indicate
        // SQL Injection, nonetheless
        GenericRDBMS("Generic SQL RDBMS", Tech.Db, "\\Qcom.ibatis.common.jdbc\\E", "\\Qorg.hibernate\\E",
                "\\Qsun.jdbc.odbc\\E", "\\Q[ODBC Driver Manager]\\E", "\\QODBC driver does not support\\E",
                "\\QSystem.Data.OleDb\\E", // System.Data.OleDb.OleDbException
                "\\Qjava.sql.SQLException\\E" // in case more specific messages were not detected!
        );

        private final String name;
        private final Tech tech;
        private final List<Pattern> errorPatterns;
        private final List<Pattern> unionErrorPatterns;

        private RDBMS(String name, Tech tech, String... errorRegexes) {
            this(name, tech, asList(errorRegexes), Collections.<String>emptyList());
        }

        private RDBMS(String name, Tech tech, List<String> errorRegexes, List<String> unionErrorRegexes) {
            this.name = name;
            this.tech = tech;

            if (errorRegexes.isEmpty()) {
                errorPatterns = Collections.emptyList();
            } else {
                errorPatterns = new ArrayList<>(errorRegexes.size());
                for (String regex : errorRegexes) {
                    errorPatterns.add(Pattern.compile(regex, AbstractAppParamPlugin.PATTERN_PARAM));
                }
            }

            if (unionErrorRegexes.isEmpty()) {
                unionErrorPatterns = Collections.emptyList();
            } else {
                unionErrorPatterns = new ArrayList<>(unionErrorRegexes.size());
                for (String regex : unionErrorRegexes) {
                    unionErrorPatterns.add(Pattern.compile(regex, AbstractAppParamPlugin.PATTERN_PARAM));
                }
            }
        }

        public String getName() {
            return name;
        }

        public Tech getTech() {
            return tech;
        }

        public boolean isGeneric() {
            return this == GenericRDBMS;
        }

        public List<Pattern> getErrorPatterns() {
            return errorPatterns;
        }

        public List<Pattern> getUnionErrorPatterns() {
            return unionErrorPatterns;
        }

        private static List<String> asList(String... strings) {
            if (strings == null || strings.length == 0) {
                return Collections.emptyList();
            }
            return Arrays.asList(strings);
        }
    }

    /**
     * always true statement for comparison in boolean based SQL injection check try the commented
     * versions first, because the law of averages says that the column being queried is more likely
     * *not* in the last where clause in a SQL query so as a result, the rest of the query needs to
     * be closed off with the comment.
     */
    private static final String[] SQL_LOGIC_AND_TRUE = { " AND 1=1" + SQL_ONE_LINE_COMMENT,
            "' AND '1'='1'" + SQL_ONE_LINE_COMMENT, "\" AND \"1\"=\"1\"" + SQL_ONE_LINE_COMMENT, " AND 1=1",
            "' AND '1'='1", "\" AND \"1\"=\"1", "%", // attack for SQL LIKE statements
            "%' " + SQL_ONE_LINE_COMMENT, // attack for SQL LIKE statements
            "%\" " + SQL_ONE_LINE_COMMENT, // attack for SQL LIKE statements
    };
    /** always false statement for comparison in boolean based SQL injection check */
    private static final String[] SQL_LOGIC_AND_FALSE = { " AND 1=2" + SQL_ONE_LINE_COMMENT,
            "' AND '1'='2'" + SQL_ONE_LINE_COMMENT, "\" AND \"1\"=\"2\"" + SQL_ONE_LINE_COMMENT, " AND 1=2",
            "' AND '1'='2", "\" AND \"1\"=\"2", "XYZABCDEFGHIJ", // attack for SQL LIKE statements
            "XYZABCDEFGHIJ' " + SQL_ONE_LINE_COMMENT, // attack for SQL LIKE statements
            "XYZABCDEFGHIJ\" " + SQL_ONE_LINE_COMMENT, // attack for SQL LIKE statements
    };
    /**
     * always true statement for comparison if no output is returned from AND in boolean based SQL
     * injection check Note that, if necessary, the code also tries a variant with the one-line
     * comment " -- " appended to the end.
     */
    private static final String[] SQL_LOGIC_OR_TRUE = { " OR 1=1" + SQL_ONE_LINE_COMMENT,
            "' OR '1'='1'" + SQL_ONE_LINE_COMMENT, "\" OR \"1\"=\"1\"" + SQL_ONE_LINE_COMMENT, " OR 1=1",
            "' OR '1'='1", "\" OR \"1\"=\"1", "%", // attack for SQL LIKE statements
            "%' " + SQL_ONE_LINE_COMMENT, // attack for SQL LIKE statements
            "%\" " + SQL_ONE_LINE_COMMENT, // attack for SQL LIKE statements
    };
    /**
     * generic UNION statements. Hoping these will cause a specific error message that we will
     * recognise
     */
    private static String[] SQL_UNION_APPENDAGES = { " UNION ALL select NULL" + SQL_ONE_LINE_COMMENT,
            "' UNION ALL select NULL" + SQL_ONE_LINE_COMMENT, "\" UNION ALL select NULL" + SQL_ONE_LINE_COMMENT,
            ") UNION ALL select NULL" + SQL_ONE_LINE_COMMENT, "') UNION ALL select NULL" + SQL_ONE_LINE_COMMENT,
            "\") UNION ALL select NULL" + SQL_ONE_LINE_COMMENT, };

    /** plugin dependencies */
    private static final String[] dependency = {};
    /** for logging. */
    private static Logger log = Logger.getLogger(TestSQLInjection.class);
    /** determines if we should output Debug level logging */
    private boolean debugEnabled = log.isDebugEnabled();

    @Override
    public int getId() {
        return 40018;
    }

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

    @Override
    public String[] getDependency() {
        return dependency;
    }

    @Override
    public boolean targets(TechSet technologies) {
        if (technologies.includes(Tech.Db)) {
            return true;
        }

        for (Tech tech : technologies.getIncludeTech()) {
            if (tech.getParent() == Tech.Db) {
                return true;
            }
        }
        return false;
    }

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

    @Override
    public int getCategory() {
        return Category.INJECTION;
    }

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

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

    /* initialise
     * Note that this method gets called each time the scanner is called.
     */
    @Override
    public void init() {
        if (this.debugEnabled) {
            log.debug("Initialising");
        }

        // DEBUG only
        // this.debugEnabled=true;
        // this.setAttackStrength(AttackStrength.LOW);

        // set up what we are allowed to do, depending on the attack strength that was set.
        if (this.getAttackStrength() == AttackStrength.LOW) {
            // do error based (if Threshold allows), and some expression based
            doErrorMaxRequests = 4;
            doExpressionBased = true;
            doExpressionMaxRequests = 4;
            doBooleanBased = false;
            doBooleanMaxRequests = 0;
            doUnionBased = false;
            doUnionMaxRequests = 0;
            doOrderByBased = false;
            doOrderByMaxRequests = 0;
            // doStackedBased = false;
            // doStackedMaxRequests = 0;

        } else if (this.getAttackStrength() == AttackStrength.MEDIUM) {
            // do some more error based (if Threshold allows), some more expression based, some
            // boolean based, and some Union based
            doErrorMaxRequests = 8;
            doExpressionBased = true;
            doExpressionMaxRequests = 8;
            doBooleanBased = true;
            doBooleanMaxRequests = 6;
            doUnionBased = true;
            doUnionMaxRequests = 5;
            doOrderByBased = false;
            doOrderByMaxRequests = 0;
            // doStackedBased = false;
            // doStackedMaxRequests = 5;

        } else if (this.getAttackStrength() == AttackStrength.HIGH) {
            // do some more error based (if Threshold allows), some more expression based, some more
            // boolean based, some union based, and some order by based
            doErrorMaxRequests = 16;
            doExpressionBased = true;
            doExpressionMaxRequests = 16;
            doBooleanBased = true;
            doBooleanMaxRequests = 20; // will not run all the LIKE attacks.. these are done at insane..
            doUnionBased = true;
            doUnionMaxRequests = 10;
            doOrderByBased = true;
            doOrderByMaxRequests = 5;
            // doStackedBased = false;
            // doStackedMaxRequests = 10;

        } else if (this.getAttackStrength() == AttackStrength.INSANE) {
            // do some more error based (if Threshold allows), some more expression based, some more
            // boolean based, some more union based, and some more order by based
            doErrorMaxRequests = 100;
            doExpressionBased = true;
            doExpressionMaxRequests = 100;
            doBooleanBased = true;
            doBooleanMaxRequests = 100;
            doUnionBased = true;
            doUnionMaxRequests = 100;
            doOrderByBased = true;
            doOrderByMaxRequests = 100;
            // doStackedBased = false;
            // doStackedMaxRequests = 100;
        }

        // if a high threshold is in place, turn off the error based, which are more prone to false
        // positives
        doSpecificErrorBased = true;
        doGenericErrorBased = true;

        if (this.getAlertThreshold() == AlertThreshold.MEDIUM) {
            doSpecificErrorBased = true;
            doGenericErrorBased = false;
        } else if (this.getAlertThreshold() == AlertThreshold.HIGH) {
            if (this.debugEnabled) {
                log.debug(
                        "Disabling the Error Based checking, since the Alert Threshold is set to High or Medium, and this type of check is notably prone to false positives");
            }
            doSpecificErrorBased = false;
            doGenericErrorBased = false;
            doErrorMaxRequests = 0;
        }

        // Only check for generic errors if not targeting a specific DB
        doGenericErrorBased &= getTechSet().includes(Tech.Db);

        if (this.debugEnabled) {
            log.debug("Doing RDBMS specific error based? " + doSpecificErrorBased);
            log.debug("Doing generic RDBMS error based? " + doGenericErrorBased);
            log.debug("Using a max of " + doErrorMaxRequests + " requests");
            log.debug("Doing expession based? " + doExpressionBased);
            log.debug("Using a max of " + doExpressionMaxRequests + " requests");
            log.debug("Using boolean based? " + doBooleanBased);
            log.debug("Using a max of " + doBooleanMaxRequests + " requests");
            log.debug("Doing UNION based? " + doUnionBased);
            log.debug("Using a max of " + doUnionMaxRequests + " requests");
            log.debug("Doing ORDER BY based? " + doOrderByBased);
            log.debug("Using a max of " + doOrderByMaxRequests + " requests");
        }
    }

    /** scans for SQL Injection vulnerabilities */
    @Override
    public void scan(HttpMessage msg, String param, String origParamValue) {
        // Note: the "value" we are passed here is escaped. we need to unescape it before handling
        // it.
        // as soon as we find a single SQL injection on the url, skip out. Do not look for SQL
        // injection on a subsequent parameter on the same URL
        // for performance reasons.
        // reinitialise each parameter.
        sqlInjectionFoundForUrl = false;
        sqlInjectionAttack = null;
        refreshedmessage = null;
        mResBodyNormalUnstripped = null;
        mResBodyNormalStripped = null;

        try {
            // reinitialise the count for each type of request, for each parameter.  We will be
            // sticking to limits defined in the attach strength logic
            countErrorBasedRequests = 0;
            countExpressionBasedRequests = 0;
            countBooleanBasedRequests = 0;
            countUnionBasedRequests = 0;
            countOrderByBasedRequests = 0;

            // Check 1: Check for Error Based SQL Injection (actual error messages).
            // for each SQL metacharacter combination to try
            for (int sqlErrorStringIndex = 0; sqlErrorStringIndex < SQL_CHECK_ERR.length && !sqlInjectionFoundForUrl
                    && doSpecificErrorBased
                    && countErrorBasedRequests < doErrorMaxRequests; sqlErrorStringIndex++) {

                // work through the attack using each of the following strings as a prefix: the
                // empty string, and the original value
                // Note: this doubles the amount of work done by the scanner, but is necessary in
                // some cases
                String[] prefixStrings;
                if (origParamValue != null) {
                    // ZAP: Removed getURLDecode()
                    prefixStrings = new String[] { "", origParamValue };
                } else {
                    prefixStrings = new String[] { "" };
                }
                for (int prefixIndex = 0; prefixIndex < prefixStrings.length
                        && !sqlInjectionFoundForUrl; prefixIndex++) {

                    // new message for each value we attack with
                    HttpMessage msg1 = getNewMsg();
                    String sqlErrValue = prefixStrings[prefixIndex] + SQL_CHECK_ERR[sqlErrorStringIndex];
                    setParameter(msg1, param, sqlErrValue);

                    // System.out.println("Attacking [" + msg + "], parameter [" + param + "] with
                    // value ["+ sqlErrValue + "]");

                    // send the message with the modified parameters
                    try {
                        sendAndReceive(msg1, false); // do not follow redirects
                    } catch (SocketException ex) {
                        if (log.isDebugEnabled())
                            log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage()
                                    + " when accessing: " + msg1.getRequestHeader().getURI().toString());
                        continue; // Something went wrong, continue to the next prefixString in the
                        // loop
                    }
                    countErrorBasedRequests++;

                    // now check the results against each pattern in turn, to try to identify a
                    // database, or even better: a specific database.
                    // Note: do NOT check the HTTP error code just yet, as the result could come
                    // back with one of various codes.
                    for (RDBMS rdbms : RDBMS.values()) {
                        if (getTechSet().includes(rdbms.getTech())
                                && checkSpecificErrors(rdbms, msg1, param, sqlErrValue)) {
                            sqlInjectionFoundForUrl = true;
                            // Save the attack string for the "Authentication Bypass" alert, if
                            // necessary
                            sqlInjectionAttack = sqlErrValue;
                            break;
                        }
                        // bale out if we were asked nicely
                        if (isStop()) {
                            log.debug("Stopping the scan due to a user request");
                            return;
                        }
                    } // end of the loop to check for RDBMS specific error messages

                    if (this.doGenericErrorBased && !sqlInjectionFoundForUrl) {
                        Iterator<Pattern> errorPatternIterator = RDBMS.GenericRDBMS.getErrorPatterns().iterator();

                        while (errorPatternIterator.hasNext() && !sqlInjectionFoundForUrl) {
                            Pattern errorPattern = errorPatternIterator.next();
                            String errorPatternRDBMS = RDBMS.GenericRDBMS.getName();

                            // if the "error message" occurs in the result of sending the modified
                            // query, but did NOT occur in the original result of the original query
                            // then we may may have a SQL Injection vulnerability
                            StringBuilder sb = new StringBuilder();
                            if (!matchBodyPattern(getBaseMsg(), errorPattern, null)
                                    && matchBodyPattern(msg1, errorPattern, sb)) {
                                // Likely a SQL Injection. Raise it
                                String extraInfo = Constant.messages.getString(
                                        MESSAGE_PREFIX + "alert.errorbased.extrainfo", errorPatternRDBMS,
                                        errorPattern.toString());
                                // raise the alert, and save the attack string for the
                                // "Authentication Bypass" alert, if necessary
                                sqlInjectionAttack = sqlErrValue;
                                bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM,
                                        getName() + " - " + errorPatternRDBMS, getDescription(), null, param,
                                        sqlInjectionAttack, extraInfo, getSolution(), sb.toString(), msg1);

                                // log it, as the RDBMS may be useful to know later (in subsequent
                                // checks, when we need to determine RDBMS specific behaviour, for
                                // instance)
                                getKb().add(getBaseMsg().getRequestHeader().getURI(), "sql/" + errorPatternRDBMS,
                                        Boolean.TRUE);

                                sqlInjectionFoundForUrl = true;
                                continue;
                            }
                            // bale out if we were asked nicely
                            if (isStop()) {
                                log.debug("Stopping the scan due to a user request");
                                return;
                            }
                        } // end of the loop to check for RDBMS specific error messages
                    }
                } // for each of the SQL_CHECK_ERR values (SQL metacharacters)
            }

            // ###############################
            // Check 4
            // New!  I haven't seen this technique documented anywhere else, but it's dead simple.
            // Let me explain.
            // See if the parameter value can simply be changed to one that *evaluates* to be the
            // same value,
            // if evaluated on a database
            // the simple check is to see if parameter "1" gives the same results as for param
            // "2-1", and different results for param "2-2"
            // for now, we try this for integer values only.
            // ###############################
            // Since the previous checks are attempting SQL injection, and may have actually
            // succeeded in modifying the database (ask me how I know?!)
            // then we cannot rely on the database contents being the same as when the original
            // query was last run (could be hours ago)
            // so to work around this, simply re-run the query again now at this point.
            // Note that we are not counting this request in our max number of requests to be issued
            refreshedmessage = getNewMsg();
            try {
                sendAndReceive(refreshedmessage, false); // do not follow redirects
            } catch (SocketException ex) {
                if (log.isDebugEnabled())
                    log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                            + refreshedmessage.getRequestHeader().getURI().toString());
                return; // Something went wrong, no point continuing
            }

            // String mResBodyNormal = getBaseMsg().getResponseBody().toString();
            mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString();
            mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue);

            if (!sqlInjectionFoundForUrl && doExpressionBased
                    && countExpressionBasedRequests < doExpressionMaxRequests) {

                // first figure out the type of the parameter..
                try {
                    // is it an integer type?
                    // ZAP: removed URLDecoding because on Variants
                    // int paramAsInt =
                    // Integer.parseInt(TestSQLInjection.getURLDecode(origParamValue));
                    int paramAsInt = Integer.parseInt(origParamValue);

                    if (this.debugEnabled) {
                        log.debug("The parameter value [" + origParamValue + "] is of type Integer");
                    }
                    // This check is implemented using two variant PLUS(+) and MULT(*)
                    try {
                        // PLUS variant check the param value "3-2" gives same result as original
                        // request and param value "4-2" gives different result if original param
                        // value is 1
                        // set the parameter value to a string value like "3-2", if the original
                        // parameter value was "1"
                        int paramPlusTwo = addWithOverflowCheck(paramAsInt, 2);
                        String modifiedParamValueForAdd = String.valueOf(paramPlusTwo) + "-2";
                        // set the parameter value to a string value like "4-2", if the original
                        // parameter value was "1"
                        int paramPlusThree = addWithOverflowCheck(paramAsInt, 3);
                        String modifiedParamValueConfirmForAdd = String.valueOf(paramPlusThree) + "-2";
                        // Do the attack for ADD variant
                        expressionBasedAttack(param, origParamValue, modifiedParamValueForAdd,
                                modifiedParamValueConfirmForAdd);
                        // bale out if we were asked nicely
                        if (isStop()) {
                            log.debug("Stopping the scan due to a user request");
                            return;
                        }
                        // MULT variant check the param value "2/2" gives same result as original
                        // request and param value "4/2" gives different result if original param
                        // value is 1
                        if (!sqlInjectionFoundForUrl && countExpressionBasedRequests < doExpressionMaxRequests) {
                            // set the parameter value to a string value like "2/2", if the original
                            // parameter value was "1"
                            int paramMultTwo = multiplyWithOverflowCheck(paramAsInt, 2);
                            String modifiedParamValueForMult = String.valueOf(paramMultTwo) + "/2";
                            // set the parameter value to a string value like "4/2", if the original
                            // parameter value was "1"
                            int paramMultFour = multiplyWithOverflowCheck(paramAsInt, 4);
                            String modifiedParamValueConfirmForMult = String.valueOf(paramMultFour) + "/2";
                            // Do the attack for MULT variant
                            expressionBasedAttack(param, origParamValue, modifiedParamValueForMult,
                                    modifiedParamValueConfirmForMult);
                            // bale out if we were asked nicely
                            if (isStop()) {
                                log.debug("Stopping the scan due to a user request");
                                return;
                            }
                        }
                    } catch (ArithmeticException ex) {
                        if (this.debugEnabled) {
                            log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage()
                                    + "When performing integer math with the parameter value [" + origParamValue
                                    + "]");
                        }
                    }
                } catch (Exception e) {
                    if (this.debugEnabled) {
                        log.debug("The parameter value [" + origParamValue + "] is NOT of type Integer");
                    }
                    // TODO: implement a similar check for string types?  This probably needs to be
                    // RDBMS specific (ie, it should not live in this scanner)
                }
            }

            // Check 2: boolean based checks.
            // the check goes like so:
            // append " and 1 = 1" to the param.  Send the query.  Check the results. Hopefully they
            // match the original results from the unmodified query,
            // *suggesting* (but not yet definitely) that we have successfully modified the query,
            // (hopefully not gotten an error message),
            // and have gotten the same results back, which is what you would expect if you added
            // the constraint " and 1 = 1" to most (but not every) SQL query.
            // So was it a fluke that we got the same results back from the modified query? Perhaps
            // the original query returned 0 rows, so adding any number of
            // constraints would change nothing?  It is still a possibility!
            // check to see if we can change the original parameter again to *restrict* the scope of
            // the query using an AND with an always false condition (AND_ERR)
            // (decreasing the results back to nothing), or to *broaden* the scope of the query
            // using an OR with an always true condition (AND_OR)
            // (increasing the results).
            // If we can successfully alter the results to our requirements, by one means or
            // another, we have found a SQL Injection vulnerability.
            // Some additional complications: assume there are 2 HTML parameters: username and
            // password, and the SQL constructed is like so:
            // select * from username where user = "$user" and password = "$password"
            // and lets assume we successfully know the type of the user field, via SQL_OR_TRUE
            // value '" OR "1"="1' (single quotes not part of the value)
            // we still have the problem that the actual SQL executed would look like so:
            // select * from username where user = "" OR "1"="1" and password = "whateveritis"
            // Since the password field is still taken into account (by virtue of the AND condition
            // on the password column), and we only inject one parameter at a time,
            // we are still not in control.
            // the solution is simple: add an end-of-line comment to the field added in (in this
            // example: the user field), so that the SQL becomes:
            // select * from username where user = "" OR "1"="1" -- and password = "whateveritis"
            // the result is that any additional constraints are commented out, and the last
            // condition to have any effect is the one whose
            // HTTP param we are manipulating.
            // Note also that because this comment only needs to be added to the "SQL_OR_TRUE" and
            // not to the equivalent SQL_AND_FALSE, because of the nature of the OR
            // and AND conditions in SQL.
            // Corollary: If a particular RDBMS does not offer the ability to comment out the
            // remainder of a line, we will not attempt to comment out anything in the query
            //            and we will simply hope that the *last* constraint in the SQL query is
            // constructed from a HTTP parameter under our control.

            if (this.debugEnabled) {
                log.debug("Doing Check 2, since check 1 did not match for "
                        + getBaseMsg().getRequestHeader().getURI());
            }

            // Since the previous checks are attempting SQL injection, and may have actually
            // succeeded in modifying the database (ask me how I know?!)
            // then we cannot rely on the database contents being the same as when the original
            // query was last run (could be hours ago)
            // so to work around this, simply re-run the query again now at this point.
            // Note that we are not counting this request in our max number of requests to be issued
            refreshedmessage = getNewMsg();
            try {
                sendAndReceive(refreshedmessage, false); // do not follow redirects
            } catch (SocketException ex) {
                if (log.isDebugEnabled())
                    log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                            + refreshedmessage.getRequestHeader().getURI().toString());
                return; // Something went wrong, no point continuing
            }

            // String mResBodyNormal = getBaseMsg().getResponseBody().toString();
            mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString();
            mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue);

            // boolean booleanBasedSqlInjectionFoundForParam = false;

            // try each of the AND syntax values in turn.
            // Which one is successful will depend on the column type of the table/view column into
            // which we are injecting the SQL.
            for (int i = 0; i < SQL_LOGIC_AND_TRUE.length && !sqlInjectionFoundForUrl && doBooleanBased
                    && countBooleanBasedRequests < doBooleanMaxRequests; i++) {
                // needs a new message for each type of AND to be issued
                HttpMessage msg2 = getNewMsg();
                // ZAP: Removed getURLDecode()
                String sqlBooleanAndTrueValue = origParamValue + SQL_LOGIC_AND_TRUE[i];
                String sqlBooleanAndFalseValue = origParamValue + SQL_LOGIC_AND_FALSE[i];

                setParameter(msg2, param, sqlBooleanAndTrueValue);

                // send the AND with an additional TRUE statement tacked onto the end. Hopefully it
                // will return the same results as the original (to find a vulnerability)
                try {
                    sendAndReceive(msg2, false); // do not follow redirects
                } catch (SocketException ex) {
                    if (log.isDebugEnabled())
                        log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                                + msg2.getRequestHeader().getURI().toString());
                    continue; // Something went wrong, continue to the next item in the loop
                }
                countBooleanBasedRequests++;

                // String resBodyAND = msg2.getResponseBody().toString();
                String resBodyANDTrueUnstripped = msg2.getResponseBody().toString();
                String resBodyANDTrueStripped = stripOffOriginalAndAttackParam(resBodyANDTrueUnstripped,
                        origParamValue, sqlBooleanAndTrueValue);

                // set up two little arrays to ease the work of checking the unstripped output, and
                // then the stripped output
                String normalBodyOutput[] = { mResBodyNormalUnstripped, mResBodyNormalStripped };
                String andTrueBodyOutput[] = { resBodyANDTrueUnstripped, resBodyANDTrueStripped };
                boolean strippedOutput[] = { false, true };

                for (int booleanStrippedUnstrippedIndex = 0; booleanStrippedUnstrippedIndex < 2; booleanStrippedUnstrippedIndex++) {
                    // if the results of the "AND 1=1" match the original query (using either the
                    // stipped or unstripped versions), we may be onto something.
                    if (andTrueBodyOutput[booleanStrippedUnstrippedIndex]
                            .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) == 0) {
                        if (this.debugEnabled) {
                            log.debug("Check 2, "
                                    + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED")
                                    + " html output for AND TRUE condition [" + sqlBooleanAndTrueValue
                                    + "] matched (refreshed) original results for "
                                    + refreshedmessage.getRequestHeader().getURI());
                        }
                        // so they match. Was it a fluke? See if we get the same result by tacking
                        // on "AND 1 = 2" to the original
                        HttpMessage msg2_and_false = getNewMsg();

                        setParameter(msg2_and_false, param, sqlBooleanAndFalseValue);

                        try {
                            sendAndReceive(msg2_and_false, false); // do not follow redirects
                        } catch (SocketException ex) {
                            if (log.isDebugEnabled())
                                log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage()
                                        + " when accessing: "
                                        + msg2_and_false.getRequestHeader().getURI().toString());
                            continue; // Something went wrong, continue on to the next item in the
                            // loop
                        }
                        countBooleanBasedRequests++;

                        // String resBodyANDFalse =
                        // stripOff(msg2_and_false.getResponseBody().toString(),
                        // SQL_LOGIC_AND_FALSE[i]);
                        // String resBodyANDFalse = msg2_and_false.getResponseBody().toString();
                        String resBodyANDFalseUnstripped = msg2_and_false.getResponseBody().toString();
                        String resBodyANDFalseStripped = stripOffOriginalAndAttackParam(resBodyANDFalseUnstripped,
                                origParamValue, sqlBooleanAndFalseValue);

                        String andFalseBodyOutput[] = { resBodyANDFalseUnstripped, resBodyANDFalseStripped };

                        // which AND False output should we compare? the stripped or the unstripped
                        // version?
                        // depends on which one we used to get to here.. use the same as that..

                        // build an always false AND query.  Result should be different to prove the
                        // SQL works.
                        if (andFalseBodyOutput[booleanStrippedUnstrippedIndex]
                                .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) != 0) {
                            if (this.debugEnabled) {
                                log.debug("Check 2, "
                                        + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED"
                                                : "UNSTRIPPED")
                                        + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue
                                        + "] differed from (refreshed) original results for "
                                        + refreshedmessage.getRequestHeader().getURI());
                            }

                            // it's different (suggesting that the "AND 1 = 2" appended on gave
                            // different results because it restricted the data set to nothing
                            // Likely a SQL Injection. Raise it
                            String extraInfo = null;
                            if (strippedOutput[booleanStrippedUnstrippedIndex]) {
                                extraInfo = Constant.messages.getString(
                                        MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue,
                                        sqlBooleanAndFalseValue, "");
                            } else {
                                extraInfo = Constant.messages.getString(
                                        MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue,
                                        sqlBooleanAndFalseValue, "NOT ");
                            }
                            extraInfo = extraInfo + "\n" + Constant.messages
                                    .getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo.dataexists");

                            // raise the alert, and save the attack string for the "Authentication
                            // Bypass" alert, if necessary
                            sqlInjectionAttack = sqlBooleanAndTrueValue;
                            bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url
                                    param, sqlInjectionAttack, extraInfo, getSolution(), "", msg2);

                            sqlInjectionFoundForUrl = true;

                            break; // No further need to loop through SQL_AND

                        } else {
                            // the results of the always false condition are the same as for the
                            // original unmodified parameter
                            // this could be because there was *no* data returned for the original
                            // unmodified parameter
                            // so consider the effect of adding comments to both the always true
                            // condition, and the always false condition
                            // the first value to try..
                            // ZAP: Removed getURLDecode()
                            String orValue = origParamValue + SQL_LOGIC_OR_TRUE[i];

                            // this is where that comment comes in handy: if the RDBMS supports
                            // one-line comments, add one in to attempt to ensure that the
                            // condition becomes one that is effectively always true, returning ALL
                            // data (or as much as possible), allowing us to pinpoint the SQL
                            // Injection
                            if (this.debugEnabled) {
                                log.debug("Check 2 , "
                                        + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED"
                                                : "UNSTRIPPED")
                                        + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue
                                        + "] SAME as (refreshed) original results for "
                                        + refreshedmessage.getRequestHeader().getURI()
                                        + " ### (forcing OR TRUE check) ");
                            }
                            HttpMessage msg2_or_true = getNewMsg();
                            setParameter(msg2_or_true, param, orValue);
                            try {
                                sendAndReceive(msg2_or_true, false); // do not follow redirects
                            } catch (SocketException ex) {
                                if (log.isDebugEnabled())
                                    log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage()
                                            + " when accessing: "
                                            + msg2_or_true.getRequestHeader().getURI().toString());
                                continue; // Something went wrong, continue on to the next item in
                                // the loop
                            }
                            countBooleanBasedRequests++;

                            // String resBodyORTrue =
                            // stripOff(msg2_or_true.getResponseBody().toString(), orValue);
                            // String resBodyORTrue = msg2_or_true.getResponseBody().toString();
                            String resBodyORTrueUnstripped = msg2_or_true.getResponseBody().toString();
                            String resBodyORTrueStripped = stripOffOriginalAndAttackParam(resBodyORTrueUnstripped,
                                    origParamValue, orValue);

                            String orTrueBodyOutput[] = { resBodyORTrueUnstripped, resBodyORTrueStripped };

                            int compareOrToOriginal = orTrueBodyOutput[booleanStrippedUnstrippedIndex]
                                    .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]);
                            if (compareOrToOriginal != 0) {

                                if (this.debugEnabled) {
                                    log.debug("Check 2, "
                                            + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED"
                                                    : "UNSTRIPPED")
                                            + " html output for OR TRUE condition [" + orValue
                                            + "] different to (refreshed) original results for "
                                            + refreshedmessage.getRequestHeader().getURI());
                                }

                                // it's different (suggesting that the "OR 1 = 1" appended on gave
                                // different results because it broadened the data set from nothing
                                // to something
                                // Likely a SQL Injection. Raise it
                                String extraInfo = null;
                                if (strippedOutput[booleanStrippedUnstrippedIndex]) {
                                    extraInfo = Constant.messages.getString(
                                            MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue,
                                            orValue, "");
                                } else {
                                    extraInfo = Constant.messages.getString(
                                            MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue,
                                            orValue, "NOT ");
                                }
                                extraInfo = extraInfo + "\n" + Constant.messages
                                        .getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo.datanotexists");

                                // raise the alert, and save the attack string for the
                                // "Authentication Bypass" alert, if necessary
                                sqlInjectionAttack = orValue;
                                bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url
                                        param, sqlInjectionAttack, extraInfo, getSolution(), "", msg2);

                                sqlInjectionFoundForUrl = true;
                                // booleanBasedSqlInjectionFoundForParam = true;  //causes us to
                                // skip past the other entries in SQL_AND.  Only one will expose a
                                // vuln for a given param, since the database column is of only 1
                                // type

                                break; // No further need to loop
                            }
                        }
                    } // if the results of the "AND 1=1" match the original query, we may be onto
                      // something.
                    else {
                        // andTrueBodyOutput[booleanStrippedUnstrippedIndex].compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex])
                        // the results of the "AND 1=1" do NOT match the original query, for
                        // whatever reason (no sql injection, or the web page is not stable)
                        if (this.debugEnabled) {
                            log.debug("Check 2, "
                                    + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED")
                                    + " html output for AND condition [" + sqlBooleanAndTrueValue
                                    + "] does NOT match the (refreshed) original results for "
                                    + refreshedmessage.getRequestHeader().getURI());
                            Patch diffpatch = DiffUtils.diff(
                                    new LinkedList<String>(Arrays
                                            .asList(normalBodyOutput[booleanStrippedUnstrippedIndex].split("\\n"))),
                                    new LinkedList<String>(Arrays.asList(
                                            andTrueBodyOutput[booleanStrippedUnstrippedIndex].split("\\n"))));

                            // int numberofDifferences = diffpatch.getDeltas().size();

                            // and convert the list of patches to a String, joining using a newline
                            StringBuilder tempDiff = new StringBuilder(250);
                            for (Delta delta : diffpatch.getDeltas()) {
                                String changeType = null;
                                if (delta.getType() == Delta.TYPE.CHANGE) {
                                    changeType = "Changed Text";
                                } else if (delta.getType() == Delta.TYPE.DELETE) {
                                    changeType = "Deleted Text";
                                } else if (delta.getType() == Delta.TYPE.INSERT) {
                                    changeType = "Inserted text";
                                } else {
                                    changeType = "Unknown change type [" + delta.getType() + "]";
                                }

                                tempDiff.append("\n(" + changeType + ")\n"); // blank line before
                                tempDiff.append("Output for Unmodified parameter: " + delta.getOriginal() + "\n");
                                tempDiff.append("Output for   modified parameter: " + delta.getRevised() + "\n");
                            }
                            log.debug("DIFFS: " + tempDiff);
                        }
                    }
                    // bale out if we were asked nicely
                    if (isStop()) {
                        log.debug("Stopping the scan due to a user request");
                        return;
                    }
                } // end of boolean logic output index (unstripped + stripped)
            }
            // end of check 2

            // check 2a: boolean based logic, where the original query returned *no* data. Here we
            // append " OR 1=1" in an attempt to extract *more* data
            // and then verify the results by attempting to reproduce the original results by
            // appending an " AND 1=2" condition (ie "open up first, then restrict to verify")
            // this differs from the previous logic based check since the previous check assumes
            // that the original query produced data, and tries first to restrict that data
            // (ie, it uses "restrict first, open up to verify" ).
            for (int i = 0; i < SQL_LOGIC_OR_TRUE.length && !sqlInjectionFoundForUrl && doBooleanBased
                    && countBooleanBasedRequests < doBooleanMaxRequests; i++) {
                HttpMessage msg2 = getNewMsg();
                String sqlBooleanOrTrueValue = origParamValue + SQL_LOGIC_OR_TRUE[i];
                String sqlBooleanAndFalseValue = origParamValue + SQL_LOGIC_AND_FALSE[i];

                setParameter(msg2, param, sqlBooleanOrTrueValue);
                try {
                    sendAndReceive(msg2, false); // do not follow redirects
                } catch (SocketException ex) {
                    if (log.isDebugEnabled())
                        log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                                + msg2.getRequestHeader().getURI().toString());
                    continue; // Something went wrong, continue on to the next item in the loop
                }
                countBooleanBasedRequests++;

                String resBodyORTrueUnstripped = msg2.getResponseBody().toString();

                // if the results of the "OR 1=1" exceed the original query (unstripped, by more
                // than a 20% size difference, say), we may be onto something.
                // TODO: change the percentage difference threshold based on the alert threshold
                if ((resBodyORTrueUnstripped.length() > (mResBodyNormalUnstripped.length() * 1.2))) {
                    if (this.debugEnabled) {
                        log.debug("Check 2a, unstripped html output for OR TRUE condition [" + sqlBooleanOrTrueValue
                                + "] produced sufficiently larger results than the original message");
                    }
                    // if we can also restrict it back to the original results by appending a " and
                    // 1=2", then "Winner Winner, Chicken Dinner".
                    HttpMessage msg2_and_false = getNewMsg();
                    setParameter(msg2_and_false, param, sqlBooleanAndFalseValue);
                    try {
                        sendAndReceive(msg2_and_false, false); // do not follow redirects
                    } catch (SocketException ex) {
                        if (log.isDebugEnabled())
                            log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage()
                                    + " when accessing: " + msg2_and_false.getRequestHeader().getURI().toString());
                        continue; // Something went wrong, continue on to the next item in the loop
                    }
                    countBooleanBasedRequests++;

                    String resBodyANDFalseUnstripped = msg2_and_false.getResponseBody().toString();
                    String resBodyANDFalseStripped = stripOffOriginalAndAttackParam(resBodyANDFalseUnstripped,
                            origParamValue, sqlBooleanAndFalseValue);

                    // does the "AND 1=2" version produce the same as the original (for
                    // stripped/unstripped versions)
                    boolean verificationUsingUnstripped = resBodyANDFalseUnstripped
                            .compareTo(mResBodyNormalUnstripped) == 0;
                    boolean verificationUsingStripped = resBodyANDFalseStripped
                            .compareTo(mResBodyNormalStripped) == 0;
                    if (verificationUsingUnstripped || verificationUsingStripped) {
                        if (this.debugEnabled) {
                            log.debug("Check 2, " + (verificationUsingStripped ? "STRIPPED" : "UNSTRIPPED")
                                    + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue
                                    + "] matches the (refreshed) original results");
                        }
                        // Likely a SQL Injection. Raise it
                        String extraInfo = null;
                        if (verificationUsingStripped) {
                            extraInfo = Constant.messages.getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo",
                                    sqlBooleanOrTrueValue, sqlBooleanAndFalseValue, "");
                        } else {
                            extraInfo = Constant.messages.getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo",
                                    sqlBooleanOrTrueValue, sqlBooleanAndFalseValue, "NOT ");
                        }
                        extraInfo = extraInfo + "\n" + Constant.messages
                                .getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo.datanotexists");

                        // raise the alert, and save the attack string for the "Authentication
                        // Bypass" alert, if necessary
                        sqlInjectionAttack = sqlBooleanOrTrueValue;
                        bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url
                                param, sqlInjectionAttack, extraInfo, getSolution(), "", msg2);

                        sqlInjectionFoundForUrl = true;

                        break; // No further need to loop
                    }
                }
            }
            // end of check 2a

            // Check 3: UNION based
            // for each SQL UNION combination to try
            for (int sqlUnionStringIndex = 0; sqlUnionStringIndex < SQL_UNION_APPENDAGES.length
                    && !sqlInjectionFoundForUrl && doUnionBased
                    && countUnionBasedRequests < doUnionMaxRequests; sqlUnionStringIndex++) {

                // new message for each value we attack with
                HttpMessage msg3 = getNewMsg();
                String sqlUnionValue = origParamValue + SQL_UNION_APPENDAGES[sqlUnionStringIndex];
                setParameter(msg3, param, sqlUnionValue);
                // send the message with the modified parameters
                try {
                    sendAndReceive(msg3, false); // do not follow redirects
                } catch (SocketException ex) {
                    if (log.isDebugEnabled())
                        log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                                + msg3.getRequestHeader().getURI().toString());
                    continue; // Something went wrong, continue on to the next item in the loop
                }
                countUnionBasedRequests++;

                // now check the results.. look first for UNION specific error messages in the
                // output that were not there in the original output
                // and failing that, look for generic RDBMS specific error messages
                // TODO: maybe also try looking at a differentiation based approach?? Prone to false
                // positives though.
                for (RDBMS rdbms : RDBMS.values()) {
                    if (getTechSet().includes(rdbms.getTech()) && checkUnionErrors(rdbms, msg3,
                            mResBodyNormalStripped, refreshedmessage.getRequestHeader().getURI(), param,
                            origParamValue, sqlUnionValue)) {
                        sqlInjectionFoundForUrl = true;
                        // Save the attack string for the "Authentication Bypass" alert, if
                        // necessary
                        sqlInjectionAttack = sqlUnionValue;
                        break;
                    }
                    // bale out if we were asked nicely
                    if (isStop()) {
                        log.debug("Stopping the scan due to a user request");
                        return;
                    }
                } // end of the loop to check for RDBMS specific UNION error messages
            } //// for each SQL UNION combination to try
              // end of check 3

            // ###############################

            // check for columns used in the "order by" clause of a SQL statement. earlier tests
            // will likely not catch these

            // append on " ASC -- " to the end of the original parameter. Grab the results.
            // if the results are different to the original (unmodified parameter) results, then
            // bale
            // if the results are the same as for the original parameter value, then the parameter
            // *might* be influencing the order by
            //   try again for "DESC": append on " DESC -- " to the end of the original parameter.
            // Grab the results.
            //   if the results are the same as the original (unmodified parameter) results, then bale
            //   (the results are not under our control, or there is no difference in the ordering,
            // for some reason: 0 or 1 rows only, or ordering
            //   by the first column alone is not sufficient to change the ordering of the data.)
            //   if the results were different to the original (unmodified parameter) results, then
            //      SQL injection!!

            // Since the previous checks are attempting SQL injection, and may have actually
            // succeeded in modifying the database (ask me how I know?!)
            // then we cannot rely on the database contents being the same as when the original
            // query was last run (could be hours ago)
            // so to work around this, simply re-run the query again now at this point.
            // Note that we are not counting this request in our max number of requests to be issued
            refreshedmessage = getNewMsg();
            try {
                sendAndReceive(refreshedmessage, false); // do not follow redirects
            } catch (SocketException ex) {
                if (log.isDebugEnabled())
                    log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                            + refreshedmessage.getRequestHeader().getURI().toString());
                return; // Something went wrong, no point continuing
            }

            // String mResBodyNormal = getBaseMsg().getResponseBody().toString();
            mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString();
            mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue);

            if (!sqlInjectionFoundForUrl && doOrderByBased && countOrderByBasedRequests < doOrderByMaxRequests) {

                // ZAP: Removed getURLDecode()
                String modifiedParamValue = origParamValue + " ASC " + SQL_ONE_LINE_COMMENT;

                HttpMessage msg5 = getNewMsg();
                setParameter(msg5, param, modifiedParamValue);

                try {
                    sendAndReceive(msg5, false); // do not follow redirects
                } catch (SocketException ex) {
                    if (log.isDebugEnabled())
                        log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                                + msg5.getRequestHeader().getURI().toString());
                    return; // Something went wrong, no point continuing
                }
                countOrderByBasedRequests++;

                String modifiedAscendingOutputUnstripped = msg5.getResponseBody().toString();
                String modifiedAscendingOutputStripped = stripOffOriginalAndAttackParam(
                        modifiedAscendingOutputUnstripped, origParamValue, modifiedParamValue);

                // set up two little arrays to ease the work of checking the unstripped output, and
                // then the stripped output
                String normalBodyOutput[] = { mResBodyNormalUnstripped, mResBodyNormalStripped };
                String ascendingBodyOutput[] = { modifiedAscendingOutputUnstripped,
                        modifiedAscendingOutputStripped };
                boolean strippedOutput[] = { false, true };

                for (int booleanStrippedUnstrippedIndex = 0; booleanStrippedUnstrippedIndex < 2; booleanStrippedUnstrippedIndex++) {
                    // if the results of the modified request match the original query, we may be
                    // onto something.
                    if (ascendingBodyOutput[booleanStrippedUnstrippedIndex]
                            .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) == 0) {
                        if (this.debugEnabled) {
                            log.debug("Check X, "
                                    + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED")
                                    + " html output for modified Order By parameter [" + modifiedParamValue
                                    + "] matched (refreshed) original results for "
                                    + refreshedmessage.getRequestHeader().getURI());
                        }
                        // confirm that a different parameter value generates different output, to
                        // minimise false positives

                        // use the descending order this time
                        // ZAP: Removed getURLDecode()
                        String modifiedParamValueConfirm = origParamValue + " DESC " + SQL_ONE_LINE_COMMENT;

                        HttpMessage msg5Confirm = getNewMsg();
                        setParameter(msg5Confirm, param, modifiedParamValueConfirm);

                        try {
                            sendAndReceive(msg5Confirm, false); // do not follow redirects
                        } catch (SocketException ex) {
                            if (log.isDebugEnabled())
                                log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage()
                                        + " when accessing: " + msg5Confirm.getRequestHeader().getURI().toString());
                            continue; // Something went wrong, continue on to the next item in the
                            // loop
                        }
                        countOrderByBasedRequests++;

                        String confirmOrderByOutputUnstripped = msg5Confirm.getResponseBody().toString();
                        String confirmOrderByOutputStripped = stripOffOriginalAndAttackParam(
                                confirmOrderByOutputUnstripped, origParamValue, modifiedParamValueConfirm);

                        // set up two little arrays to ease the work of checking the unstripped
                        // output or the stripped output
                        String confirmOrderByBodyOutput[] = { confirmOrderByOutputUnstripped,
                                confirmOrderByOutputStripped };

                        if (confirmOrderByBodyOutput[booleanStrippedUnstrippedIndex]
                                .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) != 0) {
                            // the confirm query did not return the same results.  This means that
                            // arbitrary queries are not all producing the same page output.
                            // this means the fact we earlier reproduced the original page output
                            // with a modified parameter was not a coincidence

                            // Likely a SQL Injection. Raise it
                            String extraInfo = null;
                            if (strippedOutput[booleanStrippedUnstrippedIndex]) {
                                extraInfo = Constant.messages.getString(
                                        MESSAGE_PREFIX + "alert.orderbybased.extrainfo", modifiedParamValue, "");
                            } else {
                                extraInfo = Constant.messages.getString(
                                        MESSAGE_PREFIX + "alert.orderbybased.extrainfo", modifiedParamValue,
                                        "NOT ");
                            }

                            // raise the alert, and save the attack string for the "Authentication
                            // Bypass" alert, if necessary
                            sqlInjectionAttack = modifiedParamValue;
                            bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url
                                    param, sqlInjectionAttack, extraInfo, getSolution(), "", msg5);

                            sqlInjectionFoundForUrl = true;
                            break; // No further need to loop
                        }
                    }
                    // bale out if we were asked nicely
                    if (isStop()) {
                        log.debug("Stopping the scan due to a user request");
                        return;
                    }
                }
            }

            // ###############################

            // if a sql injection was found, we should check if the page is flagged as a login page
            // in any of the contexts.  if it is, raise an "SQL Injection - Authentication Bypass"
            // alert in addition to the alerts already raised
            if (sqlInjectionFoundForUrl) {
                boolean loginUrl = false;
                // log.debug("### A SQL Injection may lead to auth bypass..");

                // are we dealing with a login url in any of the contexts?
                ExtensionAuthentication extAuth = (ExtensionAuthentication) Control.getSingleton()
                        .getExtensionLoader().getExtension(ExtensionAuthentication.NAME);
                if (extAuth != null) {
                    URI requestUri = getBaseMsg().getRequestHeader().getURI();

                    // using the session, get the list of contexts for the url
                    List<Context> contextList = extAuth.getModel().getSession()
                            .getContextsForUrl(requestUri.getURI());

                    // now loop, and see if the url is a login url in each of the contexts in turn..
                    for (Context context : contextList) {
                        URI loginUri = extAuth.getLoginRequestURIForContext(context);
                        if (loginUri != null) {
                            if (requestUri.getScheme().equals(loginUri.getScheme())
                                    && requestUri.getHost().equals(loginUri.getHost())
                                    && requestUri.getPort() == loginUri.getPort()
                                    && requestUri.getPath().equals(loginUri.getPath())) {
                                // we got this far.. only the method (GET/POST), user details, query
                                // params, fragment, and POST params
                                // are possibly different from the login page.
                                loginUrl = true;
                                // DEBUG only
                                // log.debug("##### The right login page was found");
                                break;
                            } else {
                                // log.debug("#### This is not the login page you're looking for");
                            }
                        } else {
                            // log.debug("### This context has no login page set");
                        }
                    }
                }
                if (loginUrl) {
                    // log.debug("##### Raising auth bypass");
                    // raise the alert, using the custom name and description
                    String vulnname = Constant.messages.getString(MESSAGE_PREFIX + "authbypass.name");
                    String vulndesc = Constant.messages.getString(MESSAGE_PREFIX + "authbypass.desc");

                    // raise the alert, using the attack string stored earlier for this purpose
                    bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, vulnname, vulndesc,
                            refreshedmessage.getRequestHeader().getURI().getURI(), // url
                            param, sqlInjectionAttack, "", getSolution(), "", getBaseMsg());
                } // not a login page
            } // no sql Injection Found For Url

        } catch (InvalidRedirectLocationException e) {
            // Not an error, just means we probably attacked the redirect location
        } catch (Exception e) {
            // Do not try to internationalise this.. we need an error message in any event..
            // if it's in English, it's still better than not having it at all.
            log.error("An error occurred checking a url for SQL Injection vulnerabilities", e);
        }
    }

    private boolean checkSpecificErrors(RDBMS rdbms, HttpMessage msg1, String parameter, String attack) {
        if (rdbms.isGeneric()) {
            return false;
        }

        for (Pattern errorPattern : rdbms.getErrorPatterns()) {
            if (isStop()) {
                return false;
            }

            // if the "error message" occurs in the result of sending the modified query, but did
            // NOT occur in the original result of the original query
            // then we may may have a SQL Injection vulnerability
            StringBuilder sb = new StringBuilder();
            if (!matchBodyPattern(getBaseMsg(), errorPattern, null) && matchBodyPattern(msg1, errorPattern, sb)) {
                // Likely a SQL Injection. Raise it
                String extraInfo = Constant.messages.getString(MESSAGE_PREFIX + "alert.errorbased.extrainfo",
                        rdbms.getName(), errorPattern.toString());
                bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName() + " - " + rdbms.getName(),
                        getDescription(), null, parameter, attack, extraInfo, getSolution(), sb.toString(), msg1);

                // log it, as the RDBMS may be useful to know later (in subsequent checks, when we
                // need to determine RDBMS specific behaviour, for instance)
                getKb().add(getBaseMsg().getRequestHeader().getURI(), "sql/" + rdbms.getName(), Boolean.TRUE);

                return true;
            }
        }

        return false;
    }

    private boolean checkUnionErrors(RDBMS rdbms, HttpMessage msg, String response, URI uri, String parameter,
            String originalParam, String attack) {
        for (Pattern errorPattern : rdbms.getUnionErrorPatterns()) {
            if (isStop()) {
                return false;
            }

            // if the "error message" occurs in the result of sending the modified query, but did
            // NOT occur in the original result of the original query
            // then we may may have a SQL Injection vulnerability
            String sqlUnionBodyUnstripped = msg.getResponseBody().toString();
            String sqlUnionBodyStripped = stripOffOriginalAndAttackParam(sqlUnionBodyUnstripped, originalParam,
                    attack);

            Matcher matcherOrig = errorPattern.matcher(response);
            Matcher matcherSQLUnion = errorPattern.matcher(sqlUnionBodyStripped);
            boolean patternInOrig = matcherOrig.find();
            boolean patternInSQLUnion = matcherSQLUnion.find();

            // if (! matchBodyPattern(getBaseMsg(), errorPattern, null) && matchBodyPattern(msg,
            // errorPattern, sb)) {
            if (!patternInOrig && patternInSQLUnion) {
                // Likely a UNION Based SQL Injection (by error message). Raise it
                String extraInfo = Constant.messages.getString(MESSAGE_PREFIX + "alert.unionbased.extrainfo",
                        rdbms.getName(), errorPattern.toString());
                bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName() + " - " + rdbms.getName(),
                        getDescription(), uri.getEscapedURI(), parameter, attack, extraInfo, getSolution(),
                        matcherSQLUnion.group(), msg);

                // log it, as the RDBMS may be useful to know later (in subsequent checks, when we
                // need to determine RDBMS specific behaviour, for instance)
                getKb().add(uri, "sql/" + rdbms.getName(), Boolean.TRUE);
                return true;
            }
        }

        return false;
    }

    private void expressionBasedAttack(String param, String originalParam, String modifiedParamValue,
            String modifiedParamValueConfirm) throws IOException {
        // those of you still paying attention will note that if handled as expressions (such as by
        // a database), these represent the same value.
        HttpMessage msg = getNewMsg();
        setParameter(msg, param, modifiedParamValue);

        try {
            sendAndReceive(msg, false); // do not follow redirects
        } catch (SocketException ex) {
            if (log.isDebugEnabled())
                log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                        + msg.getRequestHeader().getURI().toString());
            return; // Something went wrong, no point continuing
        }
        countExpressionBasedRequests++;

        String modifiedExpressionOutputUnstripped = msg.getResponseBody().toString();
        String modifiedExpressionOutputStripped = stripOffOriginalAndAttackParam(modifiedExpressionOutputUnstripped,
                originalParam, modifiedParamValue);
        String normalBodyOutput = mResBodyNormalStripped;

        if (!sqlInjectionFoundForUrl && countExpressionBasedRequests < doExpressionMaxRequests) {
            // if the results of the modified request match the original query, we may be onto
            // something.
            if (modifiedExpressionOutputStripped.compareTo(normalBodyOutput) == 0) {
                if (this.debugEnabled) {
                    log.debug("Check 4, STRIPPED html output for modified expression parameter ["
                            + modifiedParamValue + "] matched (refreshed) original results for "
                            + refreshedmessage.getRequestHeader().getURI().toString());
                }
                // confirm that a different parameter value generates different output, to minimise
                // false positives
                // this time param value will be different to original value and mismatch is
                // expected in responses of original and this value
                // Note that the two values are NOT equivalent, and the param value is different to
                // the original
                HttpMessage msgConfirm = getNewMsg();
                setParameter(msgConfirm, param, modifiedParamValueConfirm);

                try {
                    sendAndReceive(msgConfirm, false); // do not follow redirects
                } catch (SocketException ex) {
                    if (log.isDebugEnabled())
                        log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: "
                                + msgConfirm.getRequestHeader().getURI().toString());
                    return; // Something went wrong
                }
                countExpressionBasedRequests++;

                String confirmExpressionOutputUnstripped = msgConfirm.getResponseBody().toString();
                String confirmExpressionOutputStripped = stripOffOriginalAndAttackParam(
                        confirmExpressionOutputUnstripped, originalParam, modifiedParamValueConfirm);

                if (confirmExpressionOutputStripped.compareTo(normalBodyOutput) != 0) {
                    // the confirm query did not return the same results.  This means that arbitrary
                    // queries are not all producing the same page output.
                    // this means the fact we earier reproduced the original page output with a
                    // modified parameter was not a coincidence

                    // Likely a SQL Injection. Raise it
                    String extraInfo = Constant.messages
                            .getString(MESSAGE_PREFIX + "alert.expressionbased.extrainfo", modifiedParamValue, "");

                    // raise the alert, and save the attack string for the "Authentication Bypass"
                    // alert, if necessary
                    sqlInjectionAttack = modifiedParamValue;
                    bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url
                            param, sqlInjectionAttack, extraInfo, getSolution(), "", msg);
                    // SQL Injection has been found
                    sqlInjectionFoundForUrl = true;
                    return;
                }
            }
            // bale out if we were asked nicely
            if (isStop()) {
                return;
            }
        }
    }

    @Override
    public int getRisk() {
        return Alert.RISK_HIGH;
    }

    /**
     * Replace body by stripping of pattern string. The URLencoded pattern will also be stripped
     * off. The URL decoded pattern will not be stripped off, as this is not necessary of rour
     * purposes, and causes issues when attempting to decode parameter values such as '%' (a single
     * percent character) This is mainly used for stripping off a testing string in HTTP response
     * for comparison against the original response. Reference: TestInjectionSQL
     *
     * @param body
     * @param pattern
     * @return
     */
    protected String stripOff(String body, String pattern) {
        if (pattern == null) {
            return body;
        }

        String urlEncodePattern = getURLEncode(pattern);
        String htmlEncodePattern1 = getHTMLEncode(pattern);
        String htmlEncodePattern2 = getHTMLEncode(urlEncodePattern);
        String result = body.replaceAll("\\Q" + pattern + "\\E", "").replaceAll("\\Q" + urlEncodePattern + "\\E",
                "");
        result = result.replaceAll("\\Q" + htmlEncodePattern1 + "\\E", "")
                .replaceAll("\\Q" + htmlEncodePattern2 + "\\E", "");
        return result;
    }

    /** Replace body by stripping off pattern strings. */
    protected String stripOffOriginalAndAttackParam(String body, String originalPattern, String attackPattern) {
        String result = this.stripOff(this.stripOff(body, attackPattern), originalPattern);
        return result;
    }

    /**
     * decode method that is aware of %, and will decode it as simply %, if it occurs
     *
     * @param msg
     * @return
     */
    public static String getURLDecode(String msg) {
        String result = "";
        try {
            result = URLDecoder.decode(msg, "UTF8");

        } catch (Exception e) {
            // if it can't decode it, return the original string!
            return msg;
        }
        return result;
    }

    // TODO:replace addWithOverflowCheck method with Math.addExact when targeting JAVA 8
    /**
     * add two numbers with Arithmetic Overflow check
     *
     * @param firstNumber
     * @param secondNumber
     * @return
     */
    private static int addWithOverflowCheck(int firstNumber, int secondNumber) {
        long result = ((long) firstNumber) + ((long) secondNumber);
        if (result > Integer.MAX_VALUE) {
            throw new ArithmeticException("Overflow occurred");
        } else if (result < Integer.MIN_VALUE) {
            throw new ArithmeticException("Underflow occurred");
        }
        return (int) result;
    }

    // TODO:replace multiplyWithOverflowCheck method with Math.multiplyExact when targeting JAVA 8
    /**
     * multiply two numbers with Arithmetic Overflow check
     *
     * @param firstNumber
     * @param secondNumber
     * @return
     */
    private static int multiplyWithOverflowCheck(int firstNumber, int secondNumber) {
        long result = ((long) firstNumber) * ((long) secondNumber);
        if (result > Integer.MAX_VALUE) {
            throw new ArithmeticException("Overflow occurred");
        } else if (result < Integer.MIN_VALUE) {
            throw new ArithmeticException("Underflow occurred");
        }
        return (int) result;
    }

    @Override
    public int getCweId() {
        return 89;
    }

    @Override
    public int getWascId() {
        return 19;
    }

    @Override
    public TechSet getTechSet() {
        TechSet techSet = super.getTechSet();
        if (techSet != null) {
            return techSet;
        }
        return TechSet.AllTech;
    }
}