CVE-2020-11002 (published 2020-04-10T19:15Z)

dropwizard-validation before versions 2.0.3 and 1.3.21 has a remote code execution vulnerability. A server-side template injection was identified in the self-validating feature enabling attackers to inject arbitrary Java EL expressions, leading to Remote Code Execution (RCE) vulnerability. If you are using a self-validating bean an upgrade to Dropwizard 1.3.21/2.0.3 or later is strongly recommended. The changes introduced in Dropwizard 1.3.19 and 2.0.2 for CVE-2020-5245 unfortunately did not fix the underlying issue completely. The issue has been fixed in dropwizard-validation 1.3.21 and 2.0.3 or later. We strongly recommend upgrading to one of these versions.

Severity: HIGH
CVSS v2 base score: 9.0 (AV:N/AC:L/Au:S/C:C/I:C/A:C)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/dropwizard/dropwizard
commit_details: repo_url: https://github.com/dropwizard/dropwizard
author: Jochen Schalanda
author_date: 2020-03-26 18:08:56+01:00
msg: Disable message interpolation in ConstraintViolations by default (#3208)

Disable message interpolation in ConstraintViolations by default but allow enabling it explicitly with `SelfValidating#escapeExpressions()`.

Additionally, `ConstraintViolations` now provides a set of methods which take a map of message parameters for interpolation.
The message parameters will be escaped by default.

Refs #3153
Refs #3157
repository: repo_url: https://github.com/dropwizard/dropwizard
repo_name: dropwizard/dropwizard
description: A damn simple library for building production-ready RESTful web services.
repo_language: Java
owner: dropwizard
file_changes: filename: InterpolationHelper.java
change_type: ModificationType.ADD
diff: @@ -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");
+    }
+}

programming_language: Java
filename: SelfValidating.java
change_type: ModificationType.MODIFY
diff: @@ -7,6 +7,7 @@ import java.lang.annotation.ElementType;
 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 @@ public @interface SelfValidating {
     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;
 }

programming_language: Java
filename: SelfValidatingValidator.java
change_type: ModificationType.MODIFY
diff: @@ -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);

programming_language: Java
filename: ViolationCollector.java
change_type: ModificationType.MODIFY
diff: @@ -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 class ViolationCollector {
      *
      * @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 @@ public class ViolationCollector {
      * @return the wrapped Hibernate ConstraintValidatorContext
      */
     public ConstraintValidatorContext getContext() {
-        return context;
+        return constraintValidatorContext;
     }
 
     /**

programming_language: Java
filename: SelfValidationTest.java
change_type: ModificationType.MODIFY
diff: @@ -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 uk.org.lidalia.slf4jtest.TestLoggerFactory;
 
 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 class SelfValidationTest {
         @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 class SelfValidationTest {
     }
 
     @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();
+    }
 }

programming_language: Java