net.continuumsecurity.web.steps.WebApplicationSteps.java Source code

Java tutorial

Introduction

Here is the source code for net.continuumsecurity.web.steps.WebApplicationSteps.java

Source

/*******************************************************************************
 *    BDD-Security, application security testing framework
 *
 * Copyright (C) `2012 Stephen de Vries`
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see `<http://www.gnu.org/licenses/>`.
 ******************************************************************************/
package net.continuumsecurity.web.steps;

import net.continuumsecurity.Config;
import net.continuumsecurity.ConfigurationException;
import net.continuumsecurity.User;
import net.continuumsecurity.UserPassCredentials;
import net.continuumsecurity.behaviour.ICaptcha;
import net.continuumsecurity.behaviour.ILogin;
import net.continuumsecurity.behaviour.ILogout;
import net.continuumsecurity.behaviour.IRecoverPassword;
import net.continuumsecurity.burpclient.BurpClient;
import net.continuumsecurity.restyburp.model.HttpMessage;
import net.continuumsecurity.restyburp.model.HttpMessageList;
import net.continuumsecurity.restyburp.model.MessageType;
import net.continuumsecurity.web.Application;
import net.continuumsecurity.web.FakeCaptchaHelper;
import net.continuumsecurity.web.StepException;
import net.continuumsecurity.web.WebApplication;
import net.continuumsecurity.web.drivers.BurpFactory;
import org.apache.log4j.Logger;
import org.jbehave.core.annotations.*;
import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.steps.Parameters;
import org.junit.Assert;
import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.fail;

public class WebApplicationSteps {
    Logger log = Logger.getLogger(WebApplicationSteps.class);
    public Application app;
    UserPassCredentials credentials;
    HttpMessage currentHttp;
    HttpMessage savedMessage;
    BurpClient burp;
    List<Cookie> sessionIds;
    Map<String, List<HttpMessage>> methodProxyMap = new HashMap<String, List<HttpMessage>>();

    public WebApplicationSteps() {

    }

    @BeforeStory
    public void setup() {
        createApp();
    }

    /*
      * This has to be called explicitly when using an examples table in order to start with a fresh browser instance,
      * because @BeforeScenario is only called once for the whole scenario, not each example.
      */
    @Given("a fresh application")
    public void createApp() {
        app = Config.createApp();
        app.enableDefaultClient();
    }

    @BeforeScenario
    public void createAppAndCredentials() {
        createApp();
        credentials = new UserPassCredentials("", "");
        sessionIds = new ArrayList<Cookie>();
    }

    @Given("the login page")
    @When("the login page is displayed")
    public void openLoginPage() {
        ((ILogin) app).openLoginPage();
    }

    @Given("the default username from: $credentialsTable")
    public void defaultUsername(ExamplesTable credentialsTable) {
        credentials.setUsername(tableToDefaultCredentials(credentialsTable).getUsername());
        log.debug("username=" + credentials.getUsername());
    }

    @Given("the default password from: $credentialsTable")
    @When("the default password is used from: $credentialsTable")
    public void defaultPassword(ExamplesTable credentialsTable) {
        credentials.setPassword(tableToDefaultCredentials(credentialsTable).getPassword());
        log.debug("password=" + credentials.getPassword());
    }

    @When("the user logs in")
    @Given("the user logs in")
    public void loginWithSetCredentials() {
        ((ILogin) app).login(credentials);
    }

    @Given("the default user logs in with credentials from: $credentialsTable")
    @When("the default user logs in with credentials from: $credentialsTable")
    public void loginFromTable(ExamplesTable credentialsTable) {
        openLoginPage();
        credentials = tableToDefaultCredentials(credentialsTable);
        loginWithSetCredentials();
    }

    @Given("the username <username>")
    public void setUsernameFromExamples(@Named("username") String username) {
        credentials.setUsername(username);
    }

    @Given("an invalid username")
    public void setInvalidUsername() {
        credentials.setUsername(Config.getIncorrectUsername());
    }

    @Given("the password <password>")
    public void setCredentialsFromExamples(@Named("password") String password) {
        credentials.setPassword(password);
    }

    @Given("an incorrect password")
    public void incorrectPassword() {
        credentials.setPassword(Config.getIncorrectPassword());
    }

    @When("the user logs in from a fresh login page")
    public void loginFromFreshPage() {
        createApp();
        openLoginPage();
        loginWithSetCredentials();
    }

    // Returns just the first row in the users' credentials table
    private UserPassCredentials tableToDefaultCredentials(ExamplesTable credentialsTable) {
        assert credentialsTable.getRowCount() > 0 : "user table must have at least 1 row";
        Parameters firstRow = credentialsTable.getRowAsParameters(0);
        String username = firstRow.valueAs("username", String.class);
        String password = firstRow.valueAs("password", String.class);

        return new UserPassCredentials(username, password);
    }

    private String findRoleByUsername(String username) {
        User user = Config.instance().getUsers().findByCredential("username", username);
        if (user != null) {
            return user.getDefaultRole();
        }
        return null;
    }

    @Then("the user with the role <role> should be logged in")
    public void isLoggedIn(@Named("role") String role) {
        log.debug("checking whether logged in");
        assertThat(((ILogin) app).isLoggedIn(role), is(true));
    }

    @Then("the user is logged in")
    public void loginSucceedsVariant2() {
        assertThat(((ILogin) app).isLoggedIn(findRoleByUsername(credentials.getUsername())), is(true));
    }

    @Then("login fails")
    @Alias("the user is not logged in")
    public void loginFails() {
        assertThat(((ILogin) app).isLoggedIn(findRoleByUsername(credentials.getUsername())), is(false));
    }

    @When("the case of the password is changed")
    public void loginWithWrongCasedPassword() {
        String wrongCasePassword = credentials.getPassword().toUpperCase();

        if (wrongCasePassword.equals(credentials.getPassword())) {
            wrongCasePassword = credentials.getPassword().toLowerCase();
            if (wrongCasePassword.equals(credentials.getPassword())) {
                throw new RuntimeException("Password doesn't have alphabetic characters, can't run this test.");
            } else {
                credentials.setPassword(wrongCasePassword);
            }
        } else {
            credentials.setPassword(wrongCasePassword);
        }
    }

    @Given("the user logs in from a fresh login page $limit times")
    public void whenTheUserLogsInFromAFreshLoginPageXTimes(int limit) {
        for (int i = 0; i < limit; i++) {
            createApp();
            openLoginPage();
            loginWithSetCredentials();
        }
    }

    @When("the password is changed to values from <value>")
    public void changePasswordTo(@Named("value") String value) {
        credentials.setPassword(value);
    }

    @When("the username is changed to values from <value>")
    public void changeUsernameTo(@Named("value") String value) {
        credentials.setUsername(value);
    }

    @When("an SQL injection <value> is appended to the username")
    public void appendValueToUsername(@Named("value") String value) {
        credentials.setUsername(credentials.getUsername() + value);
    }

    @When("the user logs out")
    public void logout() {
        ((ILogout) app).logout();
    }

    @Given("an HTTP logging driver")
    public void setBurpDriver() {
        app.enableHttpLoggingClient();
    }

    @Given("clean HTTP logs")
    @When("the HTTP logs are cleared")
    public void resetBurp() {
        burp = BurpFactory.getBurp();
        burp.reset();
    }

    @Given("the HTTP request-response containing the default credentials")
    public void findRequestWithPassword() throws UnsupportedEncodingException {
        String passwd = URLEncoder.encode(credentials.getPassword(), "UTF-8");
        String username = URLEncoder.encode(credentials.getUsername(), "UTF-8");
        HttpMessageList messageList = new HttpMessageList();
        messageList.setMessages(burp.findInRequestHistory(passwd));
        List<HttpMessage> requests = messageList.findInMessages(username, MessageType.REQUEST);
        if (requests == null || requests.size() == 0)
            throw new StepException("Could not find HTTP request with credentials: " + credentials.getUsername()
                    + " " + credentials.getPassword());
        currentHttp = requests.get(0);
    }

    @Then("the protocol should be HTTPS")
    public void protocolHttps() {
        assertThat(currentHttp.getProtocol(), equalToIgnoringCase("https"));
    }

    @Given("the HTTP request-response containing the login form")
    public void findResponseWithLoginform() throws UnsupportedEncodingException {
        String regex = "(?i)input[\\s\\w=:'\"]*type\\s*=\\s*['\"]password['\"]";
        HttpMessageList messageList = new HttpMessageList();
        messageList.setMessages(burp.findInResponseHistory(regex));
        if (messageList.messages == null || messageList.messages.size() == 0)
            throw new StepException("Could not find HTTP response with password form using regex: " + regex);
        currentHttp = messageList.messages.get(0);
    }

    @Given("the request-response is saved")
    public void saveCurrentHttp() {
        savedMessage = new HttpMessage(currentHttp);
    }

    @Then("the protocol of the current URL should be HTTPS")
    public void protocolUrlHttps() {
        log.debug("URL of login page: " + ((WebApplication) app).getDriver().getCurrentUrl());
        assertThat(((WebApplication) app).getDriver().getCurrentUrl().substring(0, 4),
                equalToIgnoringCase("https"));
    }

    @Then("the response should be the same as the saved response from the invalid username")
    public void compareResponses() {
        assertThat(savedMessage.getStatusCode(), equalTo(currentHttp.getStatusCode()));

        String incorrectUsernameResponse = savedMessage.getResponseAsString()
                .replaceAll(Config.getIncorrectUsername(), "");
        String correctUsernameResponse = currentHttp.getResponseAsString()
                .replaceAll(Config.instance().getUsers().getDefaultCredentials().get("username"), "");
        assertThat(incorrectUsernameResponse, equalTo(correctUsernameResponse));
    }

    @Then("the response status code should start with 3")
    public void statusCode3xx() {
        assertThat(Short.toString(currentHttp.getStatusCode()).substring(0, 1), equalTo("3"));
    }

    @Given("the session cookies")
    public void getSessionIds() {
        Config.instance();
        for (String name : Config.getSessionIDs()) {
            Cookie cookie = app.getCookieByName(name);
            if (cookie != null)
                sessionIds.add(cookie);
        }
    }

    @Then("the session cookies after authentication should be different from those issued before")
    public void compareSessionIds() {
        Config.instance();
        for (String name : Config.getSessionIDs()) {
            Cookie initialSessionCookie = findCookieByName(sessionIds, name);
            if (initialSessionCookie != null) {
                String existingCookieValue = findCookieByName(sessionIds, name).getValue();
                assertThat(app.getCookieByName(name).getValue(), not(initialSessionCookie.getValue()));
            } else if (app.getCookieByName(name).getValue() == null) {
                throw new RuntimeException("No session IDs found after login with name: " + name);
            }
        }
    }

    @Then("the session cookies should have the secure flag set")
    public void sessionCookiesSecureFlag() {
        Config.instance();
        for (String name : Config.getSessionIDs()) {
            assertThat(app.getCookieByName(name).isSecure(), equalTo(true));
        }
    }

    @Then("the session cookies should have the httpOnly flag set")
    public void sessionCookiesHttpOnlyFlag() {
        Config.instance();
        int numCookies = Config.getSessionIDs().size();
        int cookieCount = 0;
        for (HttpMessage message : burp.getProxyHistory()) {
            for (String name : Config.getSessionIDs()) {
                Pattern pattern = Pattern.compile(name + "=.*httponly", Pattern.CASE_INSENSITIVE);
                if (pattern.matcher(message.getResponseAsString()).find()) {
                    cookieCount++;
                }
            }
        }
        Assert.assertThat(cookieCount, equalTo(numCookies));
    }

    @Then("the password field should have the autocomplete directive set to 'off'")
    public void thenThePasswordFieldShouldHaveTheAutocompleteDirectiveSetTodisabled() {
        WebElement passwd = ((WebApplication) app).getDriver().findElement(By.xpath("//input[@type='password']"));
        assertThat(passwd.getAttribute("autocomplete"), equalToIgnoringCase("off"));
    }

    @Then("no exceptions are thrown")
    public void doNothing() {

    }

    @Then("the CAPTCHA request should be present")
    public void checkCaptchaRequestPresent() {
        if (!(app instanceof ICaptcha))
            throw new RuntimeException("Application doesn't implement ICaptcha, can't run captcha scenarios");
        ICaptcha captchaApp = (ICaptcha) app;
        try {
            assertThat(captchaApp.getCaptchaImage(), notNullValue());
        } catch (NoSuchElementException nse) {
            fail("Captcha image not found.");
        }
    }

    @Given("a CAPTCHA solver that always fails")
    public void setIncorrectCaptchaHelper() {
        if (!(app instanceof ICaptcha))
            throw new RuntimeException("App does not implement ICaptcha, skipping.");
        ((ICaptcha) app).setCaptchaHelper(new FakeCaptchaHelper((ICaptcha) app));
    }

    @When("the password recovery feature is requested")
    public void submitPasswordRecovery() {
        ((IRecoverPassword) app)
                .submitRecover(Config.instance().getUsers().getAll().get(0).getRecoverPasswordMap());
    }

    @Then("the CAPTCHA should be presented again")
    public void checkCaptchaPresent() {
        try {
            assertThat(((ICaptcha) app).getCaptchaImage(), notNullValue());
        } catch (NoSuchElementException nse) {
            fail("CAPTCHA not found");
        }
    }

    @Then("the resource name <method> and HTTP requests should be recorded and stored")
    public void recordFlowInAccessControlMap(@Named("method") String method) {
        if (methodProxyMap.get(method) != null) {
            log.info("The method: " + method + " has already been added to the map, using the existing HTTP logs");
            return;
        }
        methodProxyMap.put(method, burp.getProxyHistory());
    }

    @Given("the access control map for authorised users has been populated")
    public void checkIfMapPopulated() {
        if (methodProxyMap.size() == 0)
            throw new RuntimeException("Access control map has not been populated.");
    }

    @Then("they should see the word <verifyString> when accessing the restricted resource <method>")
    public void checkAccessToResource(@Named("verifyString") String verifyString, @Named("method") String method) {
        try {
            app.getClass().getMethod(method, null).invoke(app, null);
            //For web services, calling the method might throw an exception if access is denied.
        } catch (Exception e) {
            fail("User with credentials: " + credentials.getUsername() + " " + credentials.getPassword()
                    + " could not access the method: " + method + "()");
        }
        if (methodProxyMap.get(method) != null) {
            log.info("The method: " + method + " has already been added to the map, using the existing HTTP logs");
            return;
        }
        methodProxyMap.put(method, burp.getProxyHistory());

        try {
            Assert.assertThat(burp.findInResponseHistory(verifyString).size(), greaterThan(0));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
    }

    @Then("they should not see the word <verifyString> when accessing the restricted resource <method>")
    public void checkNoAccessToResource(@Named("verifyString") String verifyString,
            @Named("method") String method) {
        if (methodProxyMap == null || methodProxyMap.get(method).size() == 0)
            throw new ConfigurationException("No HTTP messages were recorded for the method: " + method);
        Pattern pattern = Pattern.compile(verifyString);
        boolean accessible = false;
        getSessionIds();
        for (HttpMessage message : methodProxyMap.get(method)) {
            if (!"".equals(message.getResponseBody())) {
                log.debug("Original request:\n" + message.getRequestAsString());
                log.debug("Original response:\n" + message.getResponseAsString());
                Map<String, String> cookieMap = new HashMap<String, String>();
                for (Cookie cookie : sessionIds) {
                    cookieMap.put(cookie.getName(), cookie.getValue());
                }
                HttpMessage manual = new HttpMessage(message);
                manual.replaceCookies(cookieMap);
                log.debug("Replaced request: " + manual.getRequestAsString());
                manual = burp.makeRequest(manual);
                log.debug("Response: " + manual.getResponseAsString());

                if (pattern.matcher(manual.getResponseAsString()).find()) {
                    log.debug("Found regex: " + verifyString);
                    accessible = true;
                    break;
                } else {
                    log.debug("Did not find regex: " + verifyString);
                }
            }
        }
        Assert.assertThat("Resource: " + method + " can be accessed.", accessible, equalTo(false));
    }

    public Application getWebApplication() {
        return app;
    }

    private Cookie findCookieByName(List<Cookie> cookies, String name) {
        if (cookies.size() == 0)
            return null;
        for (Cookie cookie : cookies) {
            if (cookie == null)
                return null;
            if (cookie.getName().equalsIgnoreCase(name))
                return cookie;
        }
        return null;
    }
}