Artemis Java Test Sandbox InvocationTargetException Subclass Escape
Description
Artemis Java Test Sandbox versions less than 1.7.6 are vulnerable to a sandbox escape when an attacker crafts a special subclass of InvocationTargetException. An attacker can abuse this issue to execute arbitrary Java when a victim executes the supposedly sandboxed code.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A sandbox escape in Artemis Java Test Sandbox < 1.7.6 allows arbitrary Java execution via a crafted InvocationTargetException subclass.
Vulnerability
Overview
CVE-2024-23683 affects the Artemis Java Test Sandbox (Ares), a JUnit 5 extension used to secure Java testing on the interactive learning platform Artemis. In versions prior to 1.7.6, the sandbox fails to properly sanitize exceptions that have been processed by the JUnit platform's ReflectionUtils.invokeMethod method. Specifically, an attacker can craft a special subclass of InvocationTargetException that escapes the exception sanitization because JUnit extracts the cause in a trusted context before the exception reaches Ares [2]. This root cause stems from an incomplete blacklist in the test failure processing code.
Exploitation
A malicious testee can exploit this by using generics to throw a checked exception without a throws clause, as demonstrated in the ThrowWithoutThrowsHelper class [2]. By creating a malicious subclass of InvocationTargetException (e.g., EvilInvocationTargetException), the attacker can cause the JUnit platform's ReflectionUtils.invokeMethod to catch the exception and subsequently invoke its cause. Because this extraction happens in a trusted context before Ares' security checks, the attacker's code executes with elevated privileges. The attack requires the victim to execute the sandboxed code, making it a user-triggered exploit with no additional authentication needed beyond the testing environment.
Impact
Successful exploitation allows an attacker to execute arbitrary Java code in a trusted context, effectively bypassing the sandbox and gaining full control over the system [2]. This undermines the primary security goal of Ares, which is to prevent students from crashing tests or cheating. The attacker can disable Ares and perform any operation that the testing process allows, including reading/writing files, making network connections, or executing system commands.
Mitigation
The vulnerability is addressed in version 1.7.6 of Ares. The fix extends the stack blacklist to include the org.junit.platform.commons.util.ReflectionUtils.getUnderlyingCause method, preventing the escape path [4]. Users are advised to update to version 1.7.6 or later [3]. For environments where updating is not immediately possible, a workaround involves forbidding student classes in trusted packages, as described in the project's issue tracker [2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
de.tum.in.ase:artemis-java-test-sandboxMaven | < 1.7.6 | 1.7.6 |
Affected products
1- Range: <1.7.6
Patches
1af4f28a56e2fMerge pull request from GHSA-883x-6fch-6wjx
6 files changed · +47 −6
src/main/java/de/tum/in/test/api/security/ArtemisSecurityManager.java+6 −5 modified@@ -511,19 +511,20 @@ private boolean isNotPrivileged(StackFrame stackFrame) { return !AccessController.class.getName().equals(stackFrame.getClassName()); } - private boolean isCallNotWhitelisted(String call) { + private boolean isCallNotWhitelisted(String className, String methodName) { + String call = className + "." + methodName; //$NON-NLS-1$ return SecurityConstants.STACK_BLACKLIST.stream().anyMatch(call::startsWith) || (SecurityConstants.STACK_WHITELIST.stream().noneMatch(call::startsWith) - && (configuration == null || !(configuration.whitelistedClassNames().contains(call) - || configuration.trustedPackages().stream().anyMatch(pm -> pm.matches(call))))); + && (configuration == null || !(configuration.whitelistedClassNames().contains(className) + || configuration.trustedPackages().stream().anyMatch(pm -> pm.matches(className))))); } private boolean isStackFrameNotWhitelisted(StackFrame sf) { - return isCallNotWhitelisted(sf.getClassName()); + return isCallNotWhitelisted(sf.getClassName(), sf.getMethodName()); } private boolean isStackFrameNotWhitelisted(StackTraceElement ste) { - return isCallNotWhitelisted(ste.getClassName()); + return isCallNotWhitelisted(ste.getClassName(), ste.getMethodName()); } public static Optional<StackTraceElement> firstNonWhitelisted(StackTraceElement... elements) {
src/main/java/de/tum/in/test/api/security/SecurityConstants.java+2 −1 modified@@ -15,7 +15,8 @@ public final class SecurityConstants { static final Set<String> STACK_WHITELIST = Set.of("java.", "org.junit.", "jdk.", "org.eclipse.", "com.intellij", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ "org.assertj", "org.opentest4j.", "com.sun.", "sun.", "org.apache.", "de.tum.in.test.", "net.jqwik", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "ch.qos.logback", "org.jacoco", "javax.", "org.json", SECURITY_PACKAGE_NAME); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ - static final Set<String> STACK_BLACKLIST = Set.of(BlacklistedInvoker.class.getName()); + static final Set<String> STACK_BLACKLIST = Set.of(BlacklistedInvoker.class.getName(), + "org.junit.platform.commons.util.ReflectionUtils.getUnderlyingCause"); //$NON-NLS-1$ static final Set<String> PACKAGE_USE_BLACKLIST = Set.of(SECURITY_PACKAGE_NAME, "de.tum.in.test.api.internal", //$NON-NLS-1$ "jdk.internal", "sun."); //$NON-NLS-1$ //$NON-NLS-2$
src/test/java/de/tum/in/test/api/SecurityTest.java+7 −0 modified@@ -26,6 +26,7 @@ class SecurityTest { private final String testExecuteGit = "testExecuteGit"; private final String testMaliciousExceptionA = "testMaliciousExceptionA"; private final String testMaliciousExceptionB = "testMaliciousExceptionB"; + private final String testMaliciousInvocationTargetException = "testMaliciousInvocationTargetException"; private final String testNewClassLoader = "testNewClassLoader"; private final String testNewSecurityManager = "testNewSecurityManager"; private final String tryManageProcess = "tryManageProcess"; @@ -80,6 +81,12 @@ void test_testMaliciousExceptionB() { tests.assertThatEvents().haveExactly(1, testFailedWith(testMaliciousExceptionB, SecurityException.class)); } + @TestTest + void test_testMaliciousInvocationTargetException() { + tests.assertThatEvents().haveExactly(1, + testFailedWith(testMaliciousInvocationTargetException, SecurityException.class)); + } + @TestTest void test_testNewClassLoader() { tests.assertThatEvents().haveExactly(1, testFailedWith(testNewClassLoader, SecurityException.class));
src/test/java/de/tum/in/testuser/SecurityUser.java+5 −0 modified@@ -77,6 +77,11 @@ void testMaliciousExceptionB() { assertFalse(SecurityPenguin.maliciousExceptionB()); } + @Test + void testMaliciousInvocationTargetException() throws Exception { + SecurityPenguin.maliciousInvocationTargetException(); + } + @Test void testNewClassLoader() throws IOException { SecurityPenguin.newClassLoader();
src/test/java/de/tum/in/testuser/subject/SecurityPenguin.java+5 −0 modified@@ -19,6 +19,7 @@ import org.apache.xyz.Circumvention; import org.apache.xyz.FakeTrustedClass; import org.apache.xyz.MaliciousExceptionB; +import org.apache.xyz.MaliciousInvocationTargetException; import de.tum.in.test.api.io.IOTester; @@ -49,6 +50,10 @@ public static boolean maliciousExceptionB() { return ab.get(); } + public static void maliciousInvocationTargetException() throws Exception { + throw new MaliciousInvocationTargetException(); + } + @SuppressWarnings("resource") public static void newClassLoader() throws IOException { new URLClassLoader(new URL[0]).close();
src/test/java/org/apache/xyz/MaliciousInvocationTargetException.java+22 −0 added@@ -0,0 +1,22 @@ +package org.apache.xyz; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class MaliciousInvocationTargetException extends InvocationTargetException { + + private static final long serialVersionUID = 1L; + + @Override + public Throwable getTargetException() { + try { + Files.readString(Path.of("pom.xml")); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return new Error("succeeded"); + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/ls1intum/Ares/commit/af4f28a56e2fe600d8750b3b415352a0a3217392ghsapatchWEB
- github.com/advisories/GHSA-883x-6fch-6wjxghsathird-party-advisoryADVISORY
- github.com/ls1intum/Ares/security/advisories/GHSA-883x-6fch-6wjxghsavendor-advisoryWEB
- vulncheck.com/advisories/vc-advisory-GHSA-883x-6fch-6wjxmitrethird-party-advisory
- github.com/ls1intum/Ares/issues/15ghsarelatedWEB
- github.com/ls1intum/Ares/releases/tag/1.7.6ghsarelatedWEB
News mentions
0No linked articles in our index yet.