VYPR
High severityNVD Advisory· Published Apr 10, 2020· Updated Aug 4, 2024

Remote Code Execution (RCE) vulnerability in dropwizard-validation

CVE-2020-11002

Description

Dropwizard-validation's @SelfValidating feature had a server-side template injection that allowed arbitrary Java EL expression evaluation, enabling remote code execution.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Dropwizard-validation's @SelfValidating feature had a server-side template injection that allowed arbitrary Java EL expression evaluation, enabling remote code execution.

Vulnerability

Overview

CVE-2020-11002 is a server-side template injection vulnerability in dropwizard-validation, the validation component of the Dropwizard framework. The bug resides in the @SelfValidating feature, which allows custom validation logic. The ViolationCollector class, used to report validation failures, would evaluate user-injected Java Expression Language (EL) expressions in violation messages without proper sanitization. This was a continuation of a previous incomplete fix for CVE-2020-5245 [1][2][4].

Exploitation

An attacker who can control the message string passed to ViolationCollector.addViolation() methods within a @SelfValidation method can inject arbitrary EL expressions. The attack requires that a @SelfValidating bean is used in the application. No authentication is explicitly needed if the validation endpoint is exposed to unauthenticated users; however, the attacker must be able to trigger validation of a bean they control or influence [2][4]. The EL expressions are evaluated by Hibernate Validator's constraint validation context, which by default allows such expressions [1].

Impact

Successful exploitation allows an attacker to execute arbitrary Java code on the server, with the privileges of the Dropwizard service account. This could lead to full compromise of the application and host system, including data theft, service disruption, or lateral movement within the network [3][4].

Mitigation

The vulnerability is fixed in dropwizard-validation versions 1.3.21 and 2.0.3 and later. The fix disables EL expression evaluation by default in ViolationCollector. Developers must explicitly enable it by setting SelfValidating#escapeExpressions() to false if interpolation is needed. Users unable to upgrade should sanitize any message passed to ViolationCollector.addViolation() [2][4].

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.

PackageAffected versionsPatched versions
io.dropwizard:dropwizard-validationMaven
< 1.3.211.3.21
io.dropwizard:dropwizard-validationMaven
>= 2.0.0, < 2.0.32.0.3

Affected products

2

Patches

1
d5a512f7abf9

Disable message interpolation in ConstraintViolations by default (#3208)

https://github.com/dropwizard/dropwizardJochen SchalandaMar 26, 2020via ghsa
5 files changed · +222 31
  • dropwizard-validation/src/main/java/io/dropwizard/validation/InterpolationHelper.java+38 0 added
    @@ -0,0 +1,38 @@
    +/*
    + * Hibernate Validator, declare and validate application constraints
    + *
    + * License: Apache License, Version 2.0
    + * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
    + */
    +package io.dropwizard.validation;
    +
    +import javax.annotation.Nullable;
    +import java.util.regex.Matcher;
    +import java.util.regex.Pattern;
    +
    +/**
    + * Utilities used for message interpolation.
    + *
    + * @author Guillaume Smet
    + * @since 2.0.3
    + */
    +public final class InterpolationHelper {
    +
    +    public static final char BEGIN_TERM = '{';
    +    public static final char END_TERM = '}';
    +    public static final char EL_DESIGNATOR = '$';
    +    public static final char ESCAPE_CHARACTER = '\\';
    +
    +    private static final Pattern ESCAPE_MESSAGE_PARAMETER_PATTERN = Pattern.compile("([\\" + ESCAPE_CHARACTER + BEGIN_TERM + END_TERM + EL_DESIGNATOR + "])");
    +
    +    private InterpolationHelper() {
    +    }
    +
    +    @Nullable
    +    public static String escapeMessageParameter(@Nullable String messageParameter) {
    +        if (messageParameter == null) {
    +            return null;
    +        }
    +        return ESCAPE_MESSAGE_PARAMETER_PATTERN.matcher(messageParameter).replaceAll(Matcher.quoteReplacement(String.valueOf(ESCAPE_CHARACTER)) + "$1");
    +    }
    +}
    
  • dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidating.java+13 0 modified
    @@ -7,6 +7,7 @@
     import java.lang.annotation.Retention;
     import java.lang.annotation.RetentionPolicy;
     import java.lang.annotation.Target;
    +import java.util.Map;
     
     /**
      * The annotated element has methods annotated by
    @@ -24,4 +25,16 @@
         Class<?>[] groups() default {};
     
         Class<? extends Payload>[] payload() default {};
    +
    +    /**
    +     * Escape EL expressions to avoid template injection attacks.
    +     * <p>
    +     * This has serious security implications and you will
    +     * have to escape the violation messages added to {@link ViolationCollector} appropriately.
    +     *
    +     * @see ViolationCollector#addViolation(String, Map)
    +     * @see ViolationCollector#addViolation(String, String, Map)
    +     * @see ViolationCollector#addViolation(String, Integer, String, Map)
    +     */
    +    boolean escapeExpressions() default true;
     }
    
  • dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidatingValidator.java+3 1 modified
    @@ -31,15 +31,17 @@ public class SelfValidatingValidator implements ConstraintValidator<SelfValidati
         private final AnnotationConfiguration annotationConfiguration = new AnnotationConfiguration.StdConfiguration(AnnotationInclusion.INCLUDE_AND_INHERIT_IF_INHERITED);
         private final TypeResolver typeResolver = new TypeResolver();
         private final MemberResolver memberResolver = new MemberResolver(typeResolver);
    +    private boolean escapeExpressions = true;
     
         @Override
         public void initialize(SelfValidating constraintAnnotation) {
    +        escapeExpressions = constraintAnnotation.escapeExpressions();
         }
     
         @SuppressWarnings({"unchecked", "rawtypes"})
         @Override
         public boolean isValid(Object value, ConstraintValidatorContext context) {
    -        final ViolationCollector collector = new ViolationCollector(context);
    +        final ViolationCollector collector = new ViolationCollector(context, escapeExpressions);
             context.disableDefaultConstraintViolation();
             for (ValidationCaller caller : methodMap.computeIfAbsent(value.getClass(), this::findMethods)) {
                 caller.setValidationObject(value);
    
  • dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java+95 29 modified
    @@ -1,64 +1,116 @@
     package io.dropwizard.validation.selfvalidating;
     
    +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
    +
     import javax.annotation.Nullable;
     import javax.validation.ConstraintValidatorContext;
    -import java.util.regex.Matcher;
    -import java.util.regex.Pattern;
    +import java.util.Collections;
    +import java.util.Map;
    +
    +import static io.dropwizard.validation.InterpolationHelper.escapeMessageParameter;
     
     /**
      * 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 final ConstraintValidatorContext constraintValidatorContext;
    +    private final boolean escapeExpressions;
     
         private boolean violationOccurred = false;
    -    private ConstraintValidatorContext context;
     
    +    public ViolationCollector(ConstraintValidatorContext constraintValidatorContext) {
    +        this(constraintValidatorContext, true);
    +    }
     
    -    public ViolationCollector(ConstraintValidatorContext context) {
    -        this.context = context;
    +    public ViolationCollector(ConstraintValidatorContext constraintValidatorContext, boolean escapeExpressions) {
    +        this.constraintValidatorContext = constraintValidatorContext;
    +        this.escapeExpressions = escapeExpressions;
         }
     
         /**
          * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}.
    +     * <p>
    +     * Prefer the method with explicit message parameters if you want to interpolate the message.
          *
    -     * @param message the message of the violation (any EL expression will be escaped and not parsed)
    +     * @param message the message of the violation
    +     * @see #addViolation(String, Map)
          */
         public void addViolation(String message) {
    +        addViolation(message, Collections.emptyMap());
    +    }
    +
    +    /**
    +     * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}.
    +     *
    +     * @param message           the message of the violation
    +     * @param messageParameters a map of message parameters which can be interpolated in the violation message
    +     * @since 2.0.3
    +     */
    +    public void addViolation(String message, Map<String, Object> messageParameters) {
             violationOccurred = true;
    -        String messageTemplate = escapeEl(message);
    -        context.buildConstraintViolationWithTemplate(messageTemplate)
    +        getContextWithMessageParameters(messageParameters)
    +                .buildConstraintViolationWithTemplate(sanitizeTemplate(message))
                     .addConstraintViolation();
         }
     
         /**
          * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}.
    +     * <p>
    +     * Prefer the method with explicit message parameters if you want to interpolate the message.
          *
          * @param propertyName the name of the property
    -     * @param message      the message of the violation (any EL expression will be escaped and not parsed)
    +     * @param message      the message of the violation
    +     * @see #addViolation(String, String, Map)
          * @since 2.0.2
          */
         public void addViolation(String propertyName, String message) {
    +        addViolation(propertyName, message, Collections.emptyMap());
    +    }
    +
    +    /**
    +     * 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
    +     * @param messageParameters a map of message parameters which can be interpolated in the violation message
    +     * @since 2.0.3
    +     */
    +    public void addViolation(String propertyName, String message, Map<String, Object> messageParameters) {
             violationOccurred = true;
    -        String messageTemplate = escapeEl(message);
    -        context.buildConstraintViolationWithTemplate(messageTemplate)
    +        getContextWithMessageParameters(messageParameters)
    +                .buildConstraintViolationWithTemplate(sanitizeTemplate(message))
                     .addPropertyNode(propertyName)
                     .addConstraintViolation();
         }
     
         /**
          * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}.
    +     * Prefer the method with explicit message parameters if you want to interpolate the message.
          *
          * @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)
    +     * @see ViolationCollector#addViolation(String, Integer, String, Map)
          * @since 2.0.2
          */
         public void addViolation(String propertyName, Integer index, String message) {
    +        addViolation(propertyName, index, message, Collections.emptyMap());
    +    }
    +
    +    /**
    +     * 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
    +     * @param messageParameters a map of message parameters which can be interpolated in the violation message
    +     * @since 2.0.3
    +     */
    +    public void addViolation(String propertyName, Integer index, String message, Map<String, Object> messageParameters) {
             violationOccurred = true;
    -        String messageTemplate = escapeEl(message);
    -        context.buildConstraintViolationWithTemplate(messageTemplate)
    +        getContextWithMessageParameters(messageParameters)
    +                .buildConstraintViolationWithTemplate(sanitizeTemplate(message))
                     .addPropertyNode(propertyName)
                     .addBeanNode().inIterable().atIndex(index)
                     .addConstraintViolation();
    @@ -69,32 +121,46 @@ public void addViolation(String propertyName, Integer index, String message) {
          *
          * @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)
    +     * @param message      the message of the violation
          * @since 2.0.2
          */
         public void addViolation(String propertyName, String key, String message) {
    +        addViolation(propertyName, key, message, Collections.emptyMap());
    +    }
    +
    +    /**
    +     * 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
    +     * @param messageParameters a map of message parameters which can be interpolated in the violation message
    +     * @since 2.0.3
    +     */
    +    public void addViolation(String propertyName, String key, String message, Map<String, Object> messageParameters) {
             violationOccurred = true;
    -        String messageTemplate = escapeEl(message);
    +        final String messageTemplate = sanitizeTemplate(message);
    +        final HibernateConstraintValidatorContext context = getContextWithMessageParameters(messageParameters);
             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, "\\\\\\${");
    +    private HibernateConstraintValidatorContext getContextWithMessageParameters(Map<String, Object> messageParameters) {
    +        final HibernateConstraintValidatorContext context =
    +                constraintValidatorContext.unwrap(HibernateConstraintValidatorContext.class);
    +        for (Map.Entry<String, Object> messageParameter : messageParameters.entrySet()) {
    +            final Object value = messageParameter.getValue();
    +            final String escapedValue = value == null ? null : escapeMessageParameter(value.toString());
    +            context.addMessageParameter(messageParameter.getKey(), escapedValue);
             }
    -        m.appendTail(sb);
    +        return context;
    +    }
     
    -        return sb.toString();
    +    @Nullable
    +    private String sanitizeTemplate(@Nullable String message) {
    +        return escapeExpressions ? escapeMessageParameter(message) : message;
         }
     
         /**
    @@ -104,7 +170,7 @@ private String escapeEl(@Nullable String s) {
          * @return the wrapped Hibernate ConstraintValidatorContext
          */
         public ConstraintValidatorContext getContext() {
    -        return context;
    +        return constraintValidatorContext;
         }
     
         /**
    
  • dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java+73 1 modified
    @@ -1,5 +1,6 @@
     package io.dropwizard.validation;
     
    +import io.dropwizard.util.Maps;
     import io.dropwizard.validation.selfvalidating.SelfValidating;
     import io.dropwizard.validation.selfvalidating.SelfValidation;
     import io.dropwizard.validation.selfvalidating.ViolationCollector;
    @@ -12,6 +13,7 @@
     
     import javax.annotation.concurrent.NotThreadSafe;
     import javax.validation.Validator;
    +import java.util.Collections;
     
     import static org.assertj.core.api.Assertions.assertThat;
     
    @@ -146,12 +148,48 @@ public static class InjectionExample {
             @SelfValidation
             public void validateFail(ViolationCollector col) {
                 col.addViolation("${'value'}");
    +            col.addViolation("$\\A{1+1}");
    +            col.addViolation("{value}", Collections.singletonMap("value", "TEST"));
                 col.addViolation("${'property'}", "${'value'}");
                 col.addViolation("${'property'}", 1, "${'value'}");
                 col.addViolation("${'property'}", "${'key'}", "${'value'}");
             }
         }
     
    +    @SelfValidating(escapeExpressions = false)
    +    public static class EscapingDisabledExample {
    +        @SuppressWarnings("unused")
    +        @SelfValidation
    +        public void validateFail(ViolationCollector col) {
    +            col.addViolation("${'value'}");
    +            col.addViolation("$\\A{1+1}");
    +            col.addViolation("{value}", Collections.singletonMap("value", "TEST"));
    +            col.addViolation("${'property'}", "${'value'}");
    +            col.addViolation("${'property'}", 1, "${'value'}");
    +            col.addViolation("${'property'}", "${'key'}", "${'value'}");
    +        }
    +    }
    +
    +    @SelfValidating(escapeExpressions = false)
    +    public static class MessageParametersExample {
    +        @SuppressWarnings("unused")
    +        @SelfValidation
    +        public void validateFail(ViolationCollector col) {
    +            col.addViolation("{1+1}");
    +            col.addViolation("{value}", Collections.singletonMap("value", "VALUE"));
    +            col.addViolation("No parameter", Collections.singletonMap("value", "VALUE"));
    +            col.addViolation("{value} {unsetParameter}", Collections.singletonMap("value", "VALUE"));
    +            col.addViolation("{value", Collections.singletonMap("value", "VALUE"));
    +            col.addViolation("value}", Collections.singletonMap("value", "VALUE"));
    +            col.addViolation("{  value  }", Collections.singletonMap("value", "VALUE"));
    +            col.addViolation("Mixed ${'value'} {value}", Collections.singletonMap("value", "VALUE"));
    +            col.addViolation("Nested {value}", Collections.singletonMap("value", "${'nested'}"));
    +            col.addViolation("{property}", "{value}", Maps.of("property", "PROPERTY", "value", "VALUE"));
    +            col.addViolation("{property}", 1, "{value}", Maps.of("property", "PROPERTY", "value", "VALUE"));
    +            col.addViolation("{property}", "{key}", "{value}", Maps.of("property", "PROPERTY", "key", "KEY", "value", "VALUE"));
    +        }
    +    }
    +
         private final Validator validator = BaseValidator.newValidator();
     
         @Test
    @@ -271,13 +309,47 @@ public void giveWarningIfNoValidationMethods() {
         }
     
         @Test
    -    public void violationMessagesAreEscaped() {
    +    public void violationMessagesAreEscapedByDefault() {
             assertThat(ConstraintViolations.format(validator.validate(new InjectionExample()))).containsExactly(
    +                " $\\A{1+1}",
                     " ${'value'}",
    +                " {value}",
                     "${'property'} ${'value'}",
                     "${'property'}[${'key'}] ${'value'}",
                     "${'property'}[1] ${'value'}"
             );
             assertThat(TestLoggerFactory.getAllLoggingEvents()).isEmpty();
         }
    +
    +    @Test
    +    public void violationMessagesAreInterpolatedIfEscapingDisabled() {
    +        assertThat(ConstraintViolations.format(validator.validate(new EscapingDisabledExample()))).containsExactly(
    +                " A2",
    +                " TEST",
    +                " value",
    +                "${'property'} value",
    +                "${'property'}[${'key'}] value",
    +                "${'property'}[1] value"
    +        );
    +        assertThat(TestLoggerFactory.getAllLoggingEvents()).isEmpty();
    +    }
    +
    +    @Test
    +    public void messageParametersExample() {
    +        assertThat(ConstraintViolations.format(validator.validate(new MessageParametersExample()))).containsExactly(
    +                " Mixed value VALUE",
    +                " Nested ${'nested'}",
    +                " No parameter",
    +                " VALUE",
    +                " VALUE {unsetParameter}",
    +                " value}",
    +                " {  value  }",
    +                " {1+1}",
    +                " {value",
    +                "{property} VALUE",
    +                "{property}[1] VALUE",
    +                "{property}[{key}] 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

9

News mentions

0

No linked articles in our index yet.