CVE-2020-11002: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: 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.
CWE 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.
CVSS v3 base score: 8.8
Published: 2020-04-10T19:15Z
Repository: https://github.com/dropwizard/dropwizard
Language: Java
Files changed: dropwizard-validation/src/main/java/io/dropwizard/validation/InterpolationHelper.java, dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidating.java, dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidatingValidator.java, dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java, dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java
Commit date: 2020-03-26 18:08:56 +0100
Commit message: 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
Diff: diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/InterpolationHelper.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/InterpolationHelper.java
new file mode 100644
index 000000000..45f5491cd
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/InterpolationHelper.java
@@ -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");
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidating.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidating.java
index 343ecca3f..88af085dc 100644
--- a/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidating.java
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidating.java
@@ -5,10 +5,11 @@ import javax.validation.Payload;
 import java.lang.annotation.Documented;
 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
  * {@link io.dropwizard.validation.selfvalidating.SelfValidation}.
  * Those methods are executed on validation.
@@ -22,6 +23,18 @@ public @interface SelfValidating {
     String message() default "";
 
     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;
 }
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidatingValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidatingValidator.java
index 7ea7d34b7..9d9dd3662 100644
--- a/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidatingValidator.java
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/SelfValidatingValidator.java
@@ -29,19 +29,21 @@ public class SelfValidatingValidator implements ConstraintValidator<SelfValidati
     @SuppressWarnings("rawtypes")
     private final ConcurrentMap<Class<?>, List<ValidationCaller>> methodMap = new ConcurrentHashMap<>();
     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);
             caller.call(collector);
         }
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java
index 5c0005cb7..e4c7c47c6 100644
--- a/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java
@@ -1,112 +1,178 @@
 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();
     }
 
     /**
      * 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)
+     * @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;
     }
 
     /**
      * This method returns the wrapped context for raw access to the validation framework. If you use
      * the context to add violations make sure to call <code>setViolationOccurred(true)</code>.
      *
      * @return the wrapped Hibernate ConstraintValidatorContext
      */
     public ConstraintValidatorContext getContext() {
-        return context;
+        return constraintValidatorContext;
     }
 
     /**
      * @return if any violation was collected
      */
diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java
index 7ccf91557..a8d5863f9 100644
--- a/dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java
+++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java
@@ -1,7 +1,8 @@
 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;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -10,10 +11,11 @@ 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 java.util.Collections;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 @NotThreadSafe
 public class SelfValidationTest {
@@ -144,16 +146,52 @@ public class SelfValidationTest {
     public static class InjectionExample {
         @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 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
     public void failingExample() {
         assertThat(ConstraintViolations.format(validator.validate(new FailingExample())))
@@ -269,15 +307,49 @@ 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();
+    }
 }

Vulnerable code:         final ViolationCollector collector = new ViolationCollector(context);
import java.util.regex.Matcher;
import java.util.regex.Pattern;
    private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\$\\{");
    private ConstraintValidatorContext context;
    public ViolationCollector(ConstraintValidatorContext context) {
        this.context = context;
     * @param message the message of the violation (any EL expression will be escaped and not parsed)
        String messageTemplate = escapeEl(message);
        context.buildConstraintViolationWithTemplate(messageTemplate)
     * @param message      the message of the violation (any EL expression will be escaped and not parsed)
        String messageTemplate = escapeEl(message);
        context.buildConstraintViolationWithTemplate(messageTemplate)
        String messageTemplate = escapeEl(message);
        context.buildConstraintViolationWithTemplate(messageTemplate)
     * @param message      the message of the violation (any EL expression will be escaped and not parsed)
        String messageTemplate = escapeEl(message);
    @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();
        return context;
    public void violationMessagesAreEscaped() {
Fixed code: /*
 * 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");
    }
}
import java.util.Map;

    /**
     * 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;
    private boolean escapeExpressions = true;
        escapeExpressions = constraintAnnotation.escapeExpressions();
        final ViolationCollector collector = new ViolationCollector(context, escapeExpressions);
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;

import java.util.Collections;
import java.util.Map;

import static io.dropwizard.validation.InterpolationHelper.escapeMessageParameter;
    private final ConstraintValidatorContext constraintValidatorContext;
    private final boolean escapeExpressions;
    public ViolationCollector(ConstraintValidatorContext constraintValidatorContext) {
        this(constraintValidatorContext, true);
    }
    public ViolationCollector(ConstraintValidatorContext constraintValidatorContext, boolean escapeExpressions) {
        this.constraintValidatorContext = constraintValidatorContext;
        this.escapeExpressions = escapeExpressions;
     * <p>
     * Prefer the method with explicit message parameters if you want to interpolate the message.
     * @param message the message of the violation
     * @see #addViolation(String, Map)
        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) {
        getContextWithMessageParameters(messageParameters)
                .buildConstraintViolationWithTemplate(sanitizeTemplate(message))
     * <p>
     * Prefer the method with explicit message parameters if you want to interpolate the message.
     * @param message      the message of the violation
     * @see #addViolation(String, String, Map)
        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) {
        getContextWithMessageParameters(messageParameters)
                .buildConstraintViolationWithTemplate(sanitizeTemplate(message))
     * Prefer the method with explicit message parameters if you want to interpolate the message.
     * @see ViolationCollector#addViolation(String, Integer, String, Map)
        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) {
        getContextWithMessageParameters(messageParameters)
                .buildConstraintViolationWithTemplate(sanitizeTemplate(message))
     * @param message      the message of the violation
        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) {
        final String messageTemplate = sanitizeTemplate(message);
        final HibernateConstraintValidatorContext context = getContextWithMessageParameters(messageParameters);
    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);
        return context;
    }
    @Nullable
    private String sanitizeTemplate(@Nullable String message) {
        return escapeExpressions ? escapeMessageParameter(message) : message;
        return constraintValidatorContext;
import io.dropwizard.util.Maps;
import java.util.Collections;
            col.addViolation("$\\A{1+1}");
            col.addViolation("{value}", Collections.singletonMap("value", "TEST"));
    @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"));
        }
    }

    public void violationMessagesAreEscapedByDefault() {
                " $\\A{1+1}",
                " {value}",

    @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();
    }