CVE-2020-5245 (Java, published 2020-02-24T18:15Z)
Description: value: Dropwizard-Validation before 1.3.19, and 2.0.2 may allow arbitrary code execution on the host system, with the privileges of the Dropwizard service account, by injecting arbitrary Java Expression Language expressions when using the self-validating feature.

The issue has been fixed in dropwizard-validation 1.3.19 and 2.0.2.
Severity: HIGH (CVSS v3 base score: 8.8)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
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.
Repository: https://github.com/dropwizard/dropwizard
Files changed:
dropwizard-validation/src/main/java/io/dropwizard/validation/selfvalidating/ViolationCollector.java
dropwizard-validation/src/test/java/io/dropwizard/validation/SelfValidationTest.java
Commit message: Escape EL expressions in ViolationCollector (#3157)

Fixes #3153

Vulnerable code:
     * Adds a new violation to this collector. This also sets violationOccurred to true.
     * @param msg the message of the violation
    public void addViolation(String msg) {
        context.buildConstraintViolationWithTemplate(msg)
            .addConstraintViolation();
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;

    
    @BeforeEach @AfterEach
    
            col.addViolation(FAILED+"subclass");
        } 
            col.addViolation(FAILED+"subclass");
        } 
    
        } 
            col.addViolation(FAILED + "2");
            col.addViolation(FAILED + "3");
    public void failingExample() throws Exception {
            .containsExactlyInAnyOrder(FAILED_RESULT);
            .isEmpty();
    
    public void subClassExample() throws Exception {
            .containsExactlyInAnyOrder(
                    FAILED_RESULT,
                    FAILED_RESULT+"subclass"
            );
            .isEmpty();
    
    public void annotatedSubClassExample() throws Exception {
            .containsExactlyInAnyOrder(
                    FAILED_RESULT,
                    FAILED_RESULT+"subclass"
            );
            .isEmpty();
    
    public void overridingSubClassExample() throws Exception {
            .isEmpty();
            .isEmpty();
    public void correctExample() throws Exception {
            .isEmpty();
            .isEmpty();
    public void multipleTestingOfSameClass() throws Exception {
            .isEmpty();
            .isEmpty();
            .isEmpty();
    public void testDirectContextUsage() throws Exception {
            .containsExactlyInAnyOrder(FAILED_RESULT);
            .isEmpty();
    public void complexExample() throws Exception {
            .containsExactlyInAnyOrder(
                FAILED_RESULT + "1",
                FAILED_RESULT + "2",
                FAILED_RESULT + "3"
            );
            .isEmpty();
            .isEmpty();
            .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)
                    )
            );
    public void giveWarningIfNoValidationMethods() throws Exception {
            .isEmpty();
        .containsExactlyInAnyOrder(
                new LoggingEvent(
                        Level.WARN, 
                        "The class {} is annotated with @SelfValidating but contains no valid methods that are annotated with @SelfValidation",
                        NoValidations.class
                )
                

Fixed code:
import javax.annotation.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
    private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\$\\{");
     * Adds a new violation to this collector. This also sets {@code violationOccurred} to {@code true}.
     * @param message the message of the violation (any EL expression will be escaped and not parsed)
    public void addViolation(String message) {
        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();
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.annotation.concurrent.NotThreadSafe;
import javax.validation.Validator;

import static org.assertj.core.api.Assertions.assertThat;


    @AfterEach
    @BeforeEach
        @SuppressWarnings("unused")

        @SuppressWarnings("unused")
            col.addViolation(FAILED + "subclass");
        }
        @SuppressWarnings("unused")
            col.addViolation(FAILED + "subclass");
        }

        }
        @SuppressWarnings("unused")
        @SuppressWarnings("unused")
        @SuppressWarnings("unused")
            col.addViolation("p2", FAILED);
        @SuppressWarnings("unused")
            col.addViolation("p", 3, FAILED);
        }

        @SuppressWarnings("unused")
        @SelfValidation
        public void validateFail4(ViolationCollector col) {
            col.addViolation("p", "four", FAILED);
    @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'}");
        }
    }

    public void failingExample() {
                .containsExactlyInAnyOrder(FAILED_RESULT);
                .isEmpty();

    public void subClassExample() {
                .containsExactlyInAnyOrder(
                        FAILED_RESULT,
                        FAILED_RESULT + "subclass"
                );
                .isEmpty();

    public void annotatedSubClassExample() {
                .containsExactlyInAnyOrder(
                        FAILED_RESULT,
                        FAILED_RESULT + "subclass"
                );
                .isEmpty();

    public void overridingSubClassExample() {
                .isEmpty();
                .isEmpty();
    public void correctExample() {
                .isEmpty();
                .isEmpty();
    public void multipleTestingOfSameClass() {
                .isEmpty();
                .isEmpty();
                .isEmpty();
    public void testDirectContextUsage() {
                .containsExactlyInAnyOrder(FAILED_RESULT);
                .isEmpty();
    public void complexExample() {
                .containsExactly(
                        " failed1",
                        "p2 failed",
                        "p[3] failed",
                        "p[four] failed");
                .isEmpty();
                .isEmpty();
                .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)
                        )
                );
    public void giveWarningIfNoValidationMethods() {
                .isEmpty();
                .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();