Remote Code Execution (RCE) vulnerability in dropwizard-validation
Description
Dropwizard-Validation prior to 1.3.19 and 2.0.2 allows arbitrary code execution via EL injection in the self-validating feature.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Dropwizard-Validation prior to 1.3.19 and 2.0.2 allows arbitrary code execution via EL injection in the self-validating feature.
CVE-2020-5245 is a critical vulnerability in Dropwizard-Validation that stems from the self-validating feature using Java Expression Language (EL) interpolation without proper sanitization. The Jakarta Bean Validation specification allows message expressions to be evaluated, which can include dynamic EL statements [2]. Dropwizard-Validation directly passes user-controlled constraint violation messages into the EL evaluation context, enabling injection of arbitrary EL expressions [1].
Attackers can exploit this by providing crafted input that triggers a validation error, where the violation message itself—or attributes derived from it—contains malicious EL. No authentication is required if the application exposes validation endpoints to untrusted users; the attacker only needs to supply input that violates a constraint with an interpolated message. The EL is executed in the context of the Dropwizard service account [3].
Successful exploitation permits remote code execution on the host system with the privileges of the Dropwizard service process, leading to full compromise of the application server, data exfiltration, or lateral movement within the network. Because the execution runs with the service account's permissions, the impact can range from data corruption to complete system takeover [3].
The vulnerability is patched in Dropwizard-Validation versions 1.3.19 and 2.0.2 [4]. The fix escapes EL expressions in the ViolationCollector, preventing the interpretation of user-supplied strings as executable code [4]. Users running earlier versions should upgrade immediately; no workaround exists other than disabling the self-validating feature if patching is not possible.
AI Insight generated on May 21, 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 |
|---|---|---|
io.dropwizard:dropwizard-validationMaven | >= 1.3.0-rc1, < 1.3.19 | 1.3.19 |
io.dropwizard:dropwizard-validationMaven | >= 2.0.0, < 2.0.2 | 2.0.2 |
Affected products
2- Range: >= 1.3.0, < 1.3.19
Patches
228479f743a9dEscape EL expressions in ViolationCollector (#3160)
2 files changed · +124 −21
dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java+75 −5 modified@@ -1,12 +1,16 @@ package io.dropwizard.validation.selfvalidating; +import javax.annotation.Nullable; import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * This class is a simple wrapper around the ConstraintValidatorContext of hibernate validation. * It collects all the violations of the SelfValidation methods of an object. */ public class ViolationCollector { + private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\$\\{"); private boolean violationOccurred = false; private ConstraintValidatorContext context; @@ -17,14 +21,80 @@ public ViolationCollector(ConstraintValidatorContext context) { } /** - * Adds a new violation to this collector. This also sets violationOccurred to true. + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. * - * @param msg the message of the violation + * @param message the message of the violation (any EL expression will be escaped and not parsed) */ - public void addViolation(String msg) { + public void addViolation(String message) { violationOccurred = true; - context.buildConstraintViolationWithTemplate(msg) - .addConstraintViolation(); + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addConstraintViolation(); + } + + /** + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. + * + * @param propertyName the name of the property + * @param message the message of the violation (any EL expression will be escaped and not parsed) + * @since 1.3.19 + */ + public void addViolation(String propertyName, String message) { + violationOccurred = true; + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addPropertyNode(propertyName) + .addConstraintViolation(); + } + + /** + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. + * + * @param propertyName the name of the property with the violation + * @param index the index of the element with the violation + * @param message the message of the violation (any EL expression will be escaped and not parsed) + * @since 1.3.19 + */ + public void addViolation(String propertyName, Integer index, String message) { + violationOccurred = true; + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addPropertyNode(propertyName) + .addBeanNode().inIterable().atIndex(index) + .addConstraintViolation(); + } + + /** + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. + * + * @param propertyName the name of the property with the violation + * @param key the key of the element with the violation + * @param message the message of the violation (any EL expression will be escaped and not parsed) + * @since 1.3.19 + */ + public void addViolation(String propertyName, String key, String message) { + violationOccurred = true; + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addPropertyNode(propertyName) + .addBeanNode().inIterable().atKey(key) + .addConstraintViolation(); + } + + @Nullable + private String escapeEl(@Nullable String s) { + if (s == null || s.isEmpty()) { + return s; + } + + final Matcher m = ESCAPE_PATTERN.matcher(s); + final StringBuffer sb = new StringBuffer(s.length() + 16); + while (m.find()) { + m.appendReplacement(sb, "\\\\\\${"); + } + m.appendTail(sb); + + return sb.toString(); } /**
dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java+49 −16 modified@@ -10,11 +10,11 @@ import static org.assertj.core.api.Assertions.assertThat; public class SelfValidationTest { - private static final String FAILED = "failed"; @SelfValidating public static class FailingExample { + @SuppressWarnings("unused") @SelfValidation public void validateFail(ViolationCollector col) { col.addViolation(FAILED); @@ -23,6 +23,7 @@ public void validateFail(ViolationCollector col) { @SelfValidating public static class DirectContextExample { + @SuppressWarnings("unused") @SelfValidation public void validateFail(ViolationCollector col) { col.getContext().buildConstraintViolationWithTemplate(FAILED).addConstraintViolation(); @@ -51,34 +52,44 @@ public void validateFailAdditionalParameters(ViolationCollector col, int a) { col.addViolation(FAILED); } + @SuppressWarnings("unused") @SelfValidation public boolean validateFailReturn(ViolationCollector col) { col.addViolation(FAILED); return true; } + @SuppressWarnings("unused") @SelfValidation private void validateFailPrivate(ViolationCollector col) { col.addViolation(FAILED); } } - @SelfValidating public static class ComplexExample { + @SuppressWarnings("unused") @SelfValidation public void validateFail1(ViolationCollector col) { col.addViolation(FAILED + "1"); } + @SuppressWarnings("unused") @SelfValidation public void validateFail2(ViolationCollector col) { - col.addViolation(FAILED + "2"); + col.addViolation("p2", FAILED); } + @SuppressWarnings("unused") @SelfValidation public void validateFail3(ViolationCollector col) { - col.addViolation(FAILED + "3"); + col.addViolation("p", 3, FAILED); + } + + @SuppressWarnings("unused") + @SelfValidation + public void validateFail4(ViolationCollector col) { + col.addViolation("p", "four", FAILED); } @SuppressWarnings("unused") @@ -91,42 +102,54 @@ public void validateCorrect(ViolationCollector col) { public static class NoValidations { } + @SelfValidating + public static class InjectionExample { + @SuppressWarnings("unused") + @SelfValidation + public void validateFail(ViolationCollector col) { + col.addViolation("${'value'}"); + col.addViolation("${'property'}", "${'value'}"); + col.addViolation("${'property'}", 1, "${'value'}"); + col.addViolation("${'property'}", "${'key'}", "${'value'}"); + } + } + private final Validator validator = BaseValidator.newValidator(); @Test - public void failingExample() throws Exception { + public void failingExample() { assertThat(ConstraintViolations.format(validator.validate(new FailingExample()))) .containsOnly(" " + FAILED); } @Test - public void correctExample() throws Exception { + public void correctExample() { assertThat(ConstraintViolations.format(validator.validate(new CorrectExample()))) .isEmpty(); } @Test - public void multipleTestingOfSameClass() throws Exception { + public void multipleTestingOfSameClass() { assertThat(ConstraintViolations.format(validator.validate(new CorrectExample()))) - .isEmpty(); + .isEmpty(); assertThat(ConstraintViolations.format(validator.validate(new CorrectExample()))) .isEmpty(); } @Test - public void testDirectContextUsage() throws Exception { + public void testDirectContextUsage() { assertThat(ConstraintViolations.format(validator.validate(new DirectContextExample()))) .containsOnly(" " + FAILED); } @Test - public void complexExample() throws Exception { + public void complexExample() { assertThat(ConstraintViolations.format(validator.validate(new ComplexExample()))) - .containsOnly( - " " + FAILED + "1", - " " + FAILED + "2", - " " + FAILED + "3" - ); + .containsExactly( + " failed1", + "p2 failed", + "p[3] failed", + "p[four] failed"); } @Test @@ -136,8 +159,18 @@ public void invalidExample() throws Exception { } @Test - public void giveWarningIfNoValidationMethods() throws Exception { + public void giveWarningIfNoValidationMethods() { assertThat(ConstraintViolations.format(validator.validate(new NoValidations()))) .isEmpty(); } + + @Test + public void violationMessagesAreEscaped() { + assertThat(ConstraintViolations.format(validator.validate(new InjectionExample()))).containsExactly( + " ${'value'}", + "${'property'} ${'value'}", + "${'property'}[${'key'}] ${'value'}", + "${'property'}[1] ${'value'}" + ); + } }
d87d1e4f8e20Escape EL expressions in ViolationCollector (#3157)
2 files changed · +197 −91
dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java+75 −5 modified@@ -1,12 +1,16 @@ package io.dropwizard.validation.selfvalidating; +import javax.annotation.Nullable; import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * This class is a simple wrapper around the ConstraintValidatorContext of hibernate validation. * It collects all the violations of the SelfValidation methods of an object. */ public class ViolationCollector { + private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\$\\{"); private boolean violationOccurred = false; private ConstraintValidatorContext context; @@ -17,14 +21,80 @@ public ViolationCollector(ConstraintValidatorContext context) { } /** - * Adds a new violation to this collector. This also sets violationOccurred to true. + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. * - * @param msg the message of the violation + * @param message the message of the violation (any EL expression will be escaped and not parsed) */ - public void addViolation(String msg) { + public void addViolation(String message) { violationOccurred = true; - context.buildConstraintViolationWithTemplate(msg) - .addConstraintViolation(); + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addConstraintViolation(); + } + + /** + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. + * + * @param propertyName the name of the property + * @param message the message of the violation (any EL expression will be escaped and not parsed) + * @since 2.0.2 + */ + public void addViolation(String propertyName, String message) { + violationOccurred = true; + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addPropertyNode(propertyName) + .addConstraintViolation(); + } + + /** + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. + * + * @param propertyName the name of the property with the violation + * @param index the index of the element with the violation + * @param message the message of the violation (any EL expression will be escaped and not parsed) + * @since 2.0.2 + */ + public void addViolation(String propertyName, Integer index, String message) { + violationOccurred = true; + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addPropertyNode(propertyName) + .addBeanNode().inIterable().atIndex(index) + .addConstraintViolation(); + } + + /** + * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}. + * + * @param propertyName the name of the property with the violation + * @param key the key of the element with the violation + * @param message the message of the violation (any EL expression will be escaped and not parsed) + * @since 2.0.2 + */ + public void addViolation(String propertyName, String key, String message) { + violationOccurred = true; + String messageTemplate = escapeEl(message); + context.buildConstraintViolationWithTemplate(messageTemplate) + .addPropertyNode(propertyName) + .addBeanNode().inIterable().atKey(key) + .addConstraintViolation(); + } + + @Nullable + private String escapeEl(@Nullable String s) { + if (s == null || s.isEmpty()) { + return s; + } + + final Matcher m = ESCAPE_PATTERN.matcher(s); + final StringBuffer sb = new StringBuffer(s.length() + 16); + while (m.find()) { + m.appendReplacement(sb, "\\\\\\${"); + } + m.appendTail(sb); + + return sb.toString(); } /**
dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java+122 −86 modified@@ -1,64 +1,68 @@ package io.dropwizard.validation; -import static org.assertj.core.api.Assertions.assertThat; - -import javax.annotation.concurrent.NotThreadSafe; -import javax.validation.Validator; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import io.dropwizard.validation.selfvalidating.SelfValidating; import io.dropwizard.validation.selfvalidating.SelfValidation; import io.dropwizard.validation.selfvalidating.ViolationCollector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import uk.org.lidalia.slf4jext.Level; import uk.org.lidalia.slf4jtest.LoggingEvent; import uk.org.lidalia.slf4jtest.TestLoggerFactory; +import javax.annotation.concurrent.NotThreadSafe; +import javax.validation.Validator; + +import static org.assertj.core.api.Assertions.assertThat; + @NotThreadSafe public class SelfValidationTest { private static final String FAILED = "failed"; private static final String FAILED_RESULT = " " + FAILED; - - @BeforeEach @AfterEach + + @AfterEach + @BeforeEach public void clearAllLoggers() { //this must be a clear all because the validation runs in other threads TestLoggerFactory.clearAll(); } @SelfValidating public static class FailingExample { + @SuppressWarnings("unused") @SelfValidation public void validateFail(ViolationCollector col) { col.addViolation(FAILED); } } - + public static class SubclassExample extends FailingExample { + @SuppressWarnings("unused") @SelfValidation public void subValidateFail(ViolationCollector col) { - col.addViolation(FAILED+"subclass"); - } + col.addViolation(FAILED + "subclass"); + } } @SelfValidating public static class AnnotatedSubclassExample extends FailingExample { + @SuppressWarnings("unused") @SelfValidation public void subValidateFail(ViolationCollector col) { - col.addViolation(FAILED+"subclass"); - } + col.addViolation(FAILED + "subclass"); + } } - + public static class OverridingExample extends FailingExample { @Override public void validateFail(ViolationCollector col) { - } + } } @SelfValidating public static class DirectContextExample { + @SuppressWarnings("unused") @SelfValidation public void validateFail(ViolationCollector col) { col.getContext().buildConstraintViolationWithTemplate(FAILED).addConstraintViolation(); @@ -102,19 +106,28 @@ private void validateFailPrivate(ViolationCollector col) { @SelfValidating public static class ComplexExample { + @SuppressWarnings("unused") @SelfValidation public void validateFail1(ViolationCollector col) { col.addViolation(FAILED + "1"); } + @SuppressWarnings("unused") @SelfValidation public void validateFail2(ViolationCollector col) { - col.addViolation(FAILED + "2"); + col.addViolation("p2", FAILED); } + @SuppressWarnings("unused") @SelfValidation public void validateFail3(ViolationCollector col) { - col.addViolation(FAILED + "3"); + col.addViolation("p", 3, FAILED); + } + + @SuppressWarnings("unused") + @SelfValidation + public void validateFail4(ViolationCollector col) { + col.addViolation("p", "four", FAILED); } @SuppressWarnings("unused") @@ -127,121 +140,144 @@ public void validateCorrect(ViolationCollector col) { public static class NoValidations { } + @SelfValidating + public static class InjectionExample { + @SuppressWarnings("unused") + @SelfValidation + public void validateFail(ViolationCollector col) { + col.addViolation("${'value'}"); + col.addViolation("${'property'}", "${'value'}"); + col.addViolation("${'property'}", 1, "${'value'}"); + col.addViolation("${'property'}", "${'key'}", "${'value'}"); + } + } + private final Validator validator = BaseValidator.newValidator(); @Test - public void failingExample() throws Exception { + public void failingExample() { assertThat(ConstraintViolations.format(validator.validate(new FailingExample()))) - .containsExactlyInAnyOrder(FAILED_RESULT); + .containsExactlyInAnyOrder(FAILED_RESULT); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } - + @Test - public void subClassExample() throws Exception { + public void subClassExample() { assertThat(ConstraintViolations.format(validator.validate(new SubclassExample()))) - .containsExactlyInAnyOrder( - FAILED_RESULT, - FAILED_RESULT+"subclass" - ); + .containsExactlyInAnyOrder( + FAILED_RESULT, + FAILED_RESULT + "subclass" + ); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } - + @Test - public void annotatedSubClassExample() throws Exception { + public void annotatedSubClassExample() { assertThat(ConstraintViolations.format(validator.validate(new AnnotatedSubclassExample()))) - .containsExactlyInAnyOrder( - FAILED_RESULT, - FAILED_RESULT+"subclass" - ); + .containsExactlyInAnyOrder( + FAILED_RESULT, + FAILED_RESULT + "subclass" + ); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } - + @Test - public void overridingSubClassExample() throws Exception { + public void overridingSubClassExample() { assertThat(ConstraintViolations.format(validator.validate(new OverridingExample()))) - .isEmpty(); + .isEmpty(); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } @Test - public void correctExample() throws Exception { + public void correctExample() { assertThat(ConstraintViolations.format(validator.validate(new CorrectExample()))) - .isEmpty(); + .isEmpty(); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } @Test - public void multipleTestingOfSameClass() throws Exception { + public void multipleTestingOfSameClass() { assertThat(ConstraintViolations.format(validator.validate(new CorrectExample()))) - .isEmpty(); + .isEmpty(); assertThat(ConstraintViolations.format(validator.validate(new CorrectExample()))) - .isEmpty(); + .isEmpty(); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } @Test - public void testDirectContextUsage() throws Exception { + public void testDirectContextUsage() { assertThat(ConstraintViolations.format(validator.validate(new DirectContextExample()))) - .containsExactlyInAnyOrder(FAILED_RESULT); + .containsExactlyInAnyOrder(FAILED_RESULT); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } @Test - public void complexExample() throws Exception { + public void complexExample() { assertThat(ConstraintViolations.format(validator.validate(new ComplexExample()))) - .containsExactlyInAnyOrder( - FAILED_RESULT + "1", - FAILED_RESULT + "2", - FAILED_RESULT + "3" - ); + .containsExactly( + " failed1", + "p2 failed", + "p[3] failed", + "p[four] failed"); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .isEmpty(); + .isEmpty(); } @Test public void invalidExample() throws Exception { assertThat(ConstraintViolations.format(validator.validate(new InvalidExample()))) - .isEmpty(); + .isEmpty(); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .containsExactlyInAnyOrder( - new LoggingEvent( - Level.ERROR, - "The method {} is annotated with @SelfValidation but does not have a single parameter of type {}", - InvalidExample.class.getMethod("validateFailAdditionalParameters", ViolationCollector.class, int.class), - ViolationCollector.class - ), - new LoggingEvent( - Level.ERROR, - "The method {} is annotated with @SelfValidation but does not return void. It is ignored", - InvalidExample.class.getMethod("validateFailReturn", ViolationCollector.class) - ), - new LoggingEvent( - Level.ERROR, - "The method {} is annotated with @SelfValidation but is not public", - InvalidExample.class.getDeclaredMethod("validateFailPrivate", ViolationCollector.class) - ) - ); + .containsExactlyInAnyOrder( + new LoggingEvent( + Level.ERROR, + "The method {} is annotated with @SelfValidation but does not have a single parameter of type {}", + InvalidExample.class.getMethod("validateFailAdditionalParameters", ViolationCollector.class, int.class), + ViolationCollector.class + ), + new LoggingEvent( + Level.ERROR, + "The method {} is annotated with @SelfValidation but does not return void. It is ignored", + InvalidExample.class.getMethod("validateFailReturn", ViolationCollector.class) + ), + new LoggingEvent( + Level.ERROR, + "The method {} is annotated with @SelfValidation but is not public", + InvalidExample.class.getDeclaredMethod("validateFailPrivate", ViolationCollector.class) + ) + ); } @Test - public void giveWarningIfNoValidationMethods() throws Exception { + public void giveWarningIfNoValidationMethods() { assertThat(ConstraintViolations.format(validator.validate(new NoValidations()))) - .isEmpty(); + .isEmpty(); assertThat(TestLoggerFactory.getAllLoggingEvents()) - .containsExactlyInAnyOrder( - new LoggingEvent( - Level.WARN, - "The class {} is annotated with @SelfValidating but contains no valid methods that are annotated with @SelfValidation", - NoValidations.class - ) - + .containsExactlyInAnyOrder( + new LoggingEvent( + Level.WARN, + "The class {} is annotated with @SelfValidating but contains no valid methods that are annotated with @SelfValidation", + NoValidations.class + ) + + ); + } + + @Test + public void violationMessagesAreEscaped() { + assertThat(ConstraintViolations.format(validator.validate(new InjectionExample()))).containsExactly( + " ${'value'}", + "${'property'} ${'value'}", + "${'property'}[${'key'}] ${'value'}", + "${'property'}[1] ${'value'}" ); + assertThat(TestLoggerFactory.getAllLoggingEvents()).isEmpty(); } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/advisories/GHSA-3mcp-9wr4-cjqfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-5245ghsaADVISORY
- beanvalidation.org/2.0/spec/ghsax_refsource_MISCWEB
- docs.jboss.org/hibernate/validator/6.1/reference/en-US/html_single/ghsax_refsource_MISCWEB
- docs.oracle.com/javaee/7/tutorial/jsf-el.htmghsax_refsource_MISCWEB
- github.com/dropwizard/dropwizard/commit/28479f743a9d0aab6d0e963fc07f3dd98e8c8236ghsax_refsource_MISCWEB
- github.com/dropwizard/dropwizard/commit/d87d1e4f8e20f6494c0232bf8560c961b46db634ghsax_refsource_MISCWEB
- github.com/dropwizard/dropwizard/pull/3157ghsax_refsource_MISCWEB
- github.com/dropwizard/dropwizard/pull/3160ghsax_refsource_MISCWEB
- github.com/dropwizard/dropwizard/security/advisories/GHSA-3mcp-9wr4-cjqfghsax_refsource_CONFIRMWEB
- www.oracle.com/security-alerts/cpuapr2022.htmlghsaWEB
News mentions
0No linked articles in our index yet.