From 93c027b954d4ccce1ad82b2f5e27e22357757fa8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= <yoann@hibernate.org>
Date: Mon, 24 Feb 2020 17:23:37 +0100
Subject: [PATCH] HV-1774 Test arbitrary code injection through
 buildConstraintViolationWithTemplate()

---
 .../ConstraintValidatorContextTest.java       | 51 +++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/engine/src/test/java/org/hibernate/validator/test/constraints/ConstraintValidatorContextTest.java b/engine/src/test/java/org/hibernate/validator/test/constraints/ConstraintValidatorContextTest.java
index 6094195e3..a89a97016 100644
--- a/engine/src/test/java/org/hibernate/validator/test/constraints/ConstraintValidatorContextTest.java
+++ b/engine/src/test/java/org/hibernate/validator/test/constraints/ConstraintValidatorContextTest.java
@@ -194,6 +194,20 @@ public void testAddParameterNodeForFieldLevelConstraintCausesException() throws
 		}
 	}
 
+	@Test
+	public void testInjectionCausedByRecklessConcatenation() {
+		String maliciousPayload = "$\\A{1 + 1}";
+
+		// Simulate user entry, through a web form for example
+		MyObjectWithELInjectionRiskCausedByRecklessConcatenation object = new MyObjectWithELInjectionRiskCausedByRecklessConcatenation();
+		object.field1 = maliciousPayload;
+		Set<ConstraintViolation<MyObjectWithELInjectionRiskCausedByRecklessConcatenation>> constraintViolations = validator.validate( object );
+		assertThat( constraintViolations ).containsOnlyViolations(
+				violationOf( ValidationWithELInjectionRiskCausedByRecklessConcatenation.class )
+						.withMessage( "Value '" + maliciousPayload + "' is invalid" )
+		);
+	}
+
 	@MyClassLevelValidation
 	private static class MyObject {
 		@NotNull
@@ -264,6 +278,13 @@ public String getName() {
 		}
 	}
 
+	@ValidationWithELInjectionRiskCausedByRecklessConcatenation
+	private static class MyObjectWithELInjectionRiskCausedByRecklessConcatenation {
+
+		String field1;
+
+	}
+
 	@Retention(RUNTIME)
 	@Constraint(validatedBy = MyClassLevelValidation.Validator.class)
 	public @interface MyClassLevelValidation {
@@ -496,4 +517,34 @@ public boolean isValid(String value, ConstraintValidatorContext context) {
		}

	}
+
+	@Retention(RUNTIME)
+	@Constraint(validatedBy = ValidationWithELInjectionRiskCausedByRecklessConcatenation.Validator.class)
+	public @interface ValidationWithELInjectionRiskCausedByRecklessConcatenation {
+		String message() default "failed";
+
+		Class<?>[] groups() default { };
+
+		Class<? extends Payload>[] payload() default { };
+
+		class Validator
+				implements ConstraintValidator<ValidationWithELInjectionRiskCausedByRecklessConcatenation, MyObjectWithELInjectionRiskCausedByRecklessConcatenation> {
+
+			@Override
+			public boolean isValid(MyObjectWithELInjectionRiskCausedByRecklessConcatenation value, ConstraintValidatorContext context) {
+				context.disableDefaultConstraintViolation();
+
+				// This is bad practice: message parameters should be used instead.
+				// Regardless, it can happen and should work as well as possible.
+				context.buildConstraintViolationWithTemplate( "Value '" + escape( value.field1 ) + "' is invalid" )
+						.addConstraintViolation();
+
+				return false;
+			}
+
+			private String escape(String value) {
+				return value.replaceAll( "\\$+\\{", "{" );
+			}
+		}
+	}
 }
