Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions Dockerfile_desktop
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,29 @@ ARG ZAP_VERSION=2.17.0
ENV ZAP_VERSION=${ZAP_VERSION} \
JAVA_HOME=/config/java-jdk \
PATH="${PATH}:/config/java-jdk/bin" \
ZAP_HOME=/config/zap
ZAP_HOME=/config/zap \
ZAP_PROXY_PORT=8090

WORKDIR /config

# Install editors and gzip support
# Install Firefox, editors, and gzip support
RUN apt-get update \
&& apt-get --yes install --no-install-recommends vim nano gzip \
&& apt-get --yes install --no-install-recommends ca-certificates curl gnupg vim nano gzip \
&& install -d -m 0755 /etc/apt/keyrings \
&& curl -fsSL https://packages.mozilla.org/apt/repo-signing-key.gpg -o /etc/apt/keyrings/packages.mozilla.org.asc \
&& test "$(gpg --show-keys --with-colons /etc/apt/keyrings/packages.mozilla.org.asc | awk -F: '$1 == "fpr" { print $10; exit }')" = "35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3" \
&& echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" > /etc/apt/sources.list.d/mozilla.list \
&& printf '%s\n' 'Package: *' 'Pin: origin packages.mozilla.org' 'Pin-Priority: 1000' '' 'Package: firefox' 'Pin: release o=Ubuntu' 'Pin-Priority: -1' > /etc/apt/preferences.d/mozilla \
&& apt-get update \
&& apt-get --yes install --no-install-recommends firefox \
&& rm -rf /var/lib/apt/lists/*

# Application artifacts
COPY target/webgoat-*.jar /config/webgoat.jar
COPY config/desktop/start_webgoat.sh /config/start_webgoat.sh
COPY config/desktop/start_zap.sh /config/start_zap.sh
COPY config/desktop/WebGoat.txt /config/Desktop/
COPY config/desktop/firefox-policies.json /usr/lib/firefox/distribution/policies.json

# Runtime dependencies produced in the builder stages
COPY --from=zap /zap ${ZAP_HOME}
Expand Down
4 changes: 4 additions & 0 deletions config/desktop/WebGoat.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ You can start WebGoat and ZAP by opening a terminal and type:
./start_webgoat.sh
./start_zap.sh

Start WebGoat first, then start ZAP. Always launch Firefox using the Firefox button in ZAP's toolbar.
Firefox launched from ZAP is configured to proxy through ZAP. Starting Firefox from the desktop menu or another launcher will not proxy traffic through ZAP.
The Firefox bookmarks toolbar has a WebGoat and WebWolf folder.

Happy hacking,
Team WebGoat
23 changes: 23 additions & 0 deletions config/desktop/firefox-policies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"policies": {
"Homepage": {
"URL": "http://localhost:8080/WebGoat",
"Locked": true
},
"OverrideFirstRunPage": "",
"ManagedBookmarks": [
{
"toplevel_name": "WebGoat and WebWolf"
},
{
"name": "WebGoat",
"url": "http://localhost:8080/WebGoat"
},
{
"name": "WebWolf",
"url": "http://localhost:9090/WebWolf"
}
],
"DisplayBookmarksToolbar": "always"
}
}
28 changes: 27 additions & 1 deletion config/desktop/start_zap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
set -eu

ZAP_DIR=${ZAP_HOME:-/config/zap}
ZAP_PROXY_PORT=${ZAP_PROXY_PORT:-8090}
WEBGOAT_PAC_URL=${WEBGOAT_PAC_URL:-http://127.0.0.1:8080/WebGoat/proxy.pac}

if [ ! -d "$ZAP_DIR" ]; then
echo "ZAP directory $ZAP_DIR not found" >&2
Expand All @@ -15,4 +17,28 @@ if [ -z "${ZAP_JAR:-}" ]; then
exit 1
fi

exec /config/java-jdk/bin/java -jar "$ZAP_JAR"
KWRITE_CONFIG=$(command -v kwriteconfig6 || command -v kwriteconfig5 || true)
if [ -n "$KWRITE_CONFIG" ]; then
"$KWRITE_CONFIG" --file kioslaverc --group "Proxy Settings" --key ProxyType 2
"$KWRITE_CONFIG" --file kioslaverc --group "Proxy Settings" --key "Proxy Config Script" "$WEBGOAT_PAC_URL"
if command -v dbus-send >/dev/null 2>&1; then
dbus-send --type=signal /KIO/Scheduler org.kde.KIO.Scheduler.reparseSlaveConfiguration string:'' || true
fi
else
echo "Unable to configure KDE proxy settings automatically" >&2
fi

FIREFOX_BIN=$(command -v firefox || true)
export _JAVA_AWT_WM_NONREPARENTING=1
if [ -n "$FIREFOX_BIN" ]; then
exec /config/java-jdk/bin/java \
"-Dselenium.firefoxBinary=$FIREFOX_BIN" \
-jar "$ZAP_JAR" \
-port "$ZAP_PROXY_PORT" \
-config quickstart.launch.startPage=http://localhost:8080/WebGoat
fi
echo "Unable to locate Firefox; ZAP will start without a configured browser binary" >&2
exec /config/java-jdk/bin/java \
-jar "$ZAP_JAR" \
-port "$ZAP_PROXY_PORT" \
-config quickstart.launch.startPage=http://localhost:8080/WebGoat
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<groupId>org.owasp.webgoat</groupId>
<artifactId>webgoat</artifactId>
<version>2026.5</version>
<version>2026.6-SNAPSHOT</version>
<packaging>jar</packaging>

<name>WebGoat</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public void testHints() {
"CrossSiteScripting",
"CSRF",
"HijackSession",
"WebAuthn",
"SqlInjection",
"SqlInjectionMitigations",
"SqlInjectionAdvanced",
Expand Down
135 changes: 135 additions & 0 deletions src/it/java/org/owasp/webgoat/integration/WebAuthnIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: Copyright © 2026 WebGoat authors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.owasp.webgoat.integration;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import io.restassured.http.ContentType;
import io.restassured.response.ValidatableResponse;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.owasp.webgoat.lessons.webauthn.VirtualAuthenticator;
import org.owasp.webgoat.lessons.webauthn.WebAuthnSupport;

public class WebAuthnIntegrationTest extends IntegrationTest {

@Test
public void runTests() {
startLesson("WebAuthn", true);

// Registration: bind a new key to Jerry's account by changing the user handle
Map<String, Object> registrationOptions = post("WebAuthn/registration/options", Map.of());
Map<String, Object> credential = post("WebAuthn/authenticator/create", registrationOptions);
String jerrysUserHandle =
WebAuthnSupport.base64Url("jerry".getBytes(StandardCharsets.UTF_8));
checkAssignment(
webGoatUrlConfig.url("WebAuthn/registration/finish"),
ContentType.JSON,
json(Map.of("userHandle", jerrysUserHandle, "credential", credential)),
true);

// Authentication: replay Jerry's captured login, the challenge is never checked
String capturedLogin =
given()
.when()
.relaxedHTTPSValidation()
.cookie("JSESSIONID", getWebGoatCookie())
.get(webGoatUrlConfig.url("WebAuthn/support-ticket"))
.then()
.statusCode(200)
.extract()
.asString();
post("WebAuthn/authentication/options", Map.of("username", "jerry"));
checkAssignment(
webGoatUrlConfig.url("WebAuthn/authentication/finish"),
ContentType.JSON,
capturedLogin,
true);

// Quiz
checkAssignment(
webGoatUrlConfig.url("WebAuthn/quiz"),
Map.of(
"question_0_solution", "Solution 2",
"question_1_solution", "Solution 2",
"question_2_solution", "Solution 2",
"question_3_solution", "Solution 2",
"question_4_solution", "Solution 1"),
true);

// Bonus: a software security key against the secure implementation
URI base = URI.create(webGoatUrlConfig.getBaseUrl());
String origin = webGoatUrlConfig.getBaseUrl();
String rpId = base.getHost();
var securityKey = new VirtualAuthenticator();
Map<String, Object> skOptions = post("WebAuthn/securitykey/registration/options", Map.of());
var skCredential =
securityKey.create(
origin,
rpId,
(String) skOptions.get("challenge"),
WebAuthnSupport.fromBase64Url((String) ((Map<?, ?>) skOptions.get("user")).get("id")));
assertThat(
post("WebAuthn/securitykey/registration/finish", skCredential).get("registered"),
is(true));

Map<String, Object> loginOptions = post("WebAuthn/securitykey/authentication/options", Map.of());
List<String> allowed =
((List<?>) loginOptions.get("allowCredentials"))
.stream().map(d -> (String) ((Map<?, ?>) d).get("id")).toList();
var assertion =
securityKey.get(origin, rpId, (String) loginOptions.get("challenge"), allowed);
checkAssignment(
webGoatUrlConfig.url("WebAuthn/securitykey/authentication/finish"),
ContentType.JSON,
json(assertion),
true);

// replaying the same assertion against the secure implementation fails
post("WebAuthn/securitykey/authentication/options", Map.of());
postAssignment("WebAuthn/securitykey/authentication/finish", json(assertion))
.body("lessonCompleted", is(false))
.body("feedback", containsString("challenge mismatch"));

checkResults("WebAuthn");
}

private ValidatableResponse postAssignment(String path, String body) {
return given()
.when()
.relaxedHTTPSValidation()
.contentType(ContentType.JSON)
.cookie("JSESSIONID", getWebGoatCookie())
.body(body)
.post(webGoatUrlConfig.url(path))
.then()
.statusCode(200);
}

private Map<String, Object> post(String path, Object body) {
return given()
.when()
.relaxedHTTPSValidation()
.contentType(ContentType.JSON)
.cookie("JSESSIONID", getWebGoatCookie())
.body(json(body))
.post(webGoatUrlConfig.url(path))
.then()
.statusCode(200)
.extract()
.jsonPath()
.getMap("$");
}

private static String json(Object value) {
return WebAuthnSupport.toJson(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: Copyright © 2026 WebGoat authors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.owasp.webgoat.playwright.webgoat.lessons;

import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

import com.microsoft.playwright.Browser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.owasp.webgoat.container.lessons.LessonName;
import org.owasp.webgoat.playwright.webgoat.PlaywrightTest;
import org.owasp.webgoat.playwright.webgoat.helpers.Authentication;
import org.owasp.webgoat.playwright.webgoat.pages.lessons.WebAuthnLessonPage;

public class WebAuthnLessonUITest extends PlaywrightTest {

private WebAuthnLessonPage lessonPage;

@BeforeEach
void navigateToLesson(Browser browser) {
var lessonName = new LessonName("WebAuthn");
var page = Authentication.sylvester(browser);

this.lessonPage = new WebAuthnLessonPage(page);
lessonPage.resetLesson(lessonName);
lessonPage.open(lessonName);
}

@Test
@DisplayName("Solve the GoatBank registration and authentication assignments and the quiz")
void shouldSolveGoatBankAssignments() {
lessonPage.navigateTo(4);
lessonPage.registerKeyForJerry();
assertThat(lessonPage.registrationFeedback()).containsText("Jerry");
assertThat(lessonPage.registrationFeedback()).containsText("Well done");

lessonPage.navigateTo(6);
lessonPage.replayCapturedLogin();
assertThat(lessonPage.authenticationFeedback()).containsText("signed in as Jerry");

lessonPage.navigateTo(8);
lessonPage.solveQuiz();
}

@Test
@DisplayName("Register and sign in with a (virtual) roaming security key")
void shouldSignInWithSecurityKey() {
lessonPage.attachVirtualSecurityKey();
lessonPage.navigateTo(10);

lessonPage.registerSecurityKey();
assertThat(lessonPage.securityKeyRegistration()).containsText("\"registered\": true");
assertThat(lessonPage.securityKeyRegistration()).containsText("BE=false");

lessonPage.signInWithSecurityKey();
assertThat(lessonPage.securityKeyFeedback()).containsText("You signed in with your security key");
}
}
Loading
Loading