Skip to content

Commit e076293

Browse files
committed
HV-1816 Disable Expression Language by default for custom constraint violations
1 parent 56d443d commit e076293

File tree

18 files changed

+220
-71
lines changed

18 files changed

+220
-71
lines changed

documentation/src/main/asciidoc/ch06.asciidoc

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -173,35 +173,12 @@ It is important to add each configured constraint violation by calling `addConst
173173
Only after that the new constraint violation will be created.
174174
====
175175

176-
[[el-injection-caution]]
177-
[CAUTION]
178-
====
179-
**Be aware that the custom message template is passed directly to the Expression Language engine.**
180-
181-
Thus, you should be very careful when integrating user input in a custom message template as it will be interpreted
182-
by the Expression Language engine, which is usually not the behavior you want and **could allow malicious users to leak
183-
sensitive data or even execute arbitrary code**.
184-
185-
If you need to integrate user input in your message, you must <<section-hibernateconstraintvalidatorcontext,pass it as an expression variable>>
186-
by unwrapping the context to `HibernateConstraintValidatorContext`.
187-
188-
The following validator is very unsafe as it includes user input in the violation message.
189-
If the validated `value` contains EL expressions, they will be executed by the EL engine.
190-
191-
[source, JAVA, indent=0]
192-
----
193-
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/elinjection/UnsafeValidator.java[tags=include]
194-
----
195-
196-
The following pattern must be used instead:
176+
By default, Expression Language is not enabled for custom violations created in the `ConstraintValidatorContext`.
197177

198-
[source]
199-
----
200-
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/elinjection/SafeValidator.java[tags=include]
201-
----
178+
However, for some advanced requirements, using Expression Language might be necessary.
202179

203-
By using expression variables, Hibernate Validator properly handles escaping and EL expressions won't be executed.
204-
====
180+
In this case, you need to unwrap the `HibernateConstraintValidatorContext` and enable Expression Language explicitly.
181+
See <<section-hibernateconstraintvalidatorcontext>> for more information.
205182

206183
Refer to <<section-custom-property-paths>> to learn how to use the `ConstraintValidatorContext` API to
207184
control the property path of constraint violations for class-level constraints.

documentation/src/main/asciidoc/ch12.asciidoc

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ custom extensions for both of these interfaces.
510510
[[section-custom-constraint-validator-context]]
511511
`HibernateConstraintValidatorContext` is a subtype of `ConstraintValidatorContext` which allows you to:
512512

513+
* enable Expression Language interpolation for a particular custom violation - see below
513514
* set arbitrary parameters for interpolation via the Expression Language message interpolation
514515
facility using `HibernateConstraintValidatorContext#addExpressionVariable(String, Object)`
515516
or `HibernateConstraintValidatorContext#addMessageParameter(String, Object)`.
@@ -535,8 +536,8 @@ include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/context/My
535536
[NOTE]
536537
====
537538
Apart from the syntax, the main difference between message parameters and expression variables is that message parameters
538-
are simply interpolated whereas expression variables are interpreted using the expression language engine.
539-
In practice, it should not change anything.
539+
are simply interpolated whereas expression variables are interpreted using the Expression Language engine.
540+
In practice, use message parameters if you do not need the advanced features of an Expression Language.
540541
====
541542
+
542543
[NOTE]
@@ -550,6 +551,38 @@ You can, however, update the parameters between invocations of
550551
====
551552
* set an arbitrary dynamic payload - see <<section-dynamic-payload>>
552553

554+
By default, Expression Language interpolation is **disabled** for custom violations,
555+
this to avoid arbitrary code execution or sensitive data leak if user input is not properly escaped.
556+
557+
It is possible to enable Expression Language for a given custom violation by using `enableExpressionLanguage()` as shown in the example below:
558+
559+
[source]
560+
----
561+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/elinjection/SafeValidator.java[tags=include]
562+
----
563+
564+
In this case, the message template will be interpolated by the Expression Language engine.
565+
566+
[CAUTION]
567+
====
568+
Using `addExpressionVariable()` is the only safe way to inject a variable into an expression.
569+
570+
If you inject user input by simply concatenating the user input in the message,
571+
you will allow potential arbitrary code execution and sensitive data leak:
572+
if the user input contains valid expressions, they will be executed by the Expression Language engine.
573+
574+
Here is an example of something you should **ABSOLUTELY NOT** do:
575+
576+
[source, JAVA, indent=0]
577+
----
578+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/elinjection/UnsafeValidator.java[tags=include]
579+
----
580+
581+
In the example above, if `value`, which might be user input, contains a valid expression,
582+
it will be interpolated by the Expression Language engine,
583+
potentially leading to unsafe behaviors.
584+
====
585+
553586
==== `HibernateMessageInterpolatorContext`
554587

555588
Hibernate Validator also offers a custom extension of `MessageInterpolatorContext`, namely

documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/elinjection/SafeValidator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) {
2323
hibernateContext
2424
.addExpressionVariable( "validatedValue", value )
2525
.buildConstraintViolationWithTemplate( "${validatedValue} is not a valid ZIP code" )
26+
.enableExpressionLanguage()
2627
.addConstraintViolation();
2728

2829
return false;

documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/elinjection/UnsafeValidator.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.hibernate.validator.referenceguide.chapter06.elinjection;
22

3+
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
34
import org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload.ZipCode;
45

56
import javax.validation.ConstraintValidator;
@@ -16,9 +17,15 @@ public boolean isValid(String value, ConstraintValidatorContext context) {
1617

1718
context.disableDefaultConstraintViolation();
1819

20+
HibernateConstraintValidatorContext hibernateContext = context.unwrap(
21+
HibernateConstraintValidatorContext.class );
22+
hibernateContext.disableDefaultConstraintViolation();
23+
1924
if ( isInvalid( value ) ) {
20-
context
25+
hibernateContext
26+
// THIS IS UNSAFE, DO NOT COPY THIS EXAMPLE
2127
.buildConstraintViolationWithTemplate( value + " is not a valid ZIP code" )
28+
.enableExpressionLanguage()
2229
.addConstraintViolation();
2330

2431
return false;

engine/src/main/java/org/hibernate/validator/constraintvalidation/HibernateConstraintValidatorContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
*/
2222
public interface HibernateConstraintValidatorContext extends ConstraintValidatorContext {
2323

24+
@Override
25+
HibernateConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);
26+
2427
/**
2528
* Allows to set an additional named parameter which can be interpolated in the constraint violation message. The
2629
* variable will be available for interpolation for all constraint violations generated for this constraint.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Hibernate Validator, declare and validate application constraints
3+
*
4+
* License: Apache License, Version 2.0
5+
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
6+
*/
7+
8+
package org.hibernate.validator.constraintvalidation;
9+
10+
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder;
11+
12+
import org.hibernate.validator.Incubating;
13+
14+
public interface HibernateConstraintViolationBuilder extends ConstraintViolationBuilder {
15+
16+
/**
17+
* Enable Expression Language for the constraint violation created by this builder if the chosen
18+
* {@code MessageInterpolator} supports it.
19+
* <p>
20+
* If enabling this, you need to make sure your message template does not contain any unescaped user input (such as
21+
* the validated value): use {@code addExpressionVariable()} to inject properly escaped variables into the template.
22+
*
23+
* @since 6.2
24+
*/
25+
@Incubating
26+
HibernateConstraintViolationBuilder enableExpressionLanguage();
27+
}

engine/src/main/java/org/hibernate/validator/internal/engine/MessageInterpolatorContext.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,22 @@ public class MessageInterpolatorContext implements HibernateMessageInterpolatorC
3939
private final Map<String, Object> messageParameters;
4040
@Immutable
4141
private final Map<String, Object> expressionVariables;
42+
private final boolean expressionLanguageEnabled;
4243

4344
public MessageInterpolatorContext(ConstraintDescriptor<?> constraintDescriptor,
4445
Object validatedValue,
4546
Class<?> rootBeanType,
4647
Path propertyPath,
4748
Map<String, Object> messageParameters,
48-
Map<String, Object> expressionVariables) {
49+
Map<String, Object> expressionVariables,
50+
boolean expressionLanguageEnabled) {
4951
this.constraintDescriptor = constraintDescriptor;
5052
this.validatedValue = validatedValue;
5153
this.rootBeanType = rootBeanType;
5254
this.propertyPath = propertyPath;
5355
this.messageParameters = toImmutableMap( messageParameters );
5456
this.expressionVariables = toImmutableMap( expressionVariables );
57+
this.expressionLanguageEnabled = expressionLanguageEnabled;
5558
}
5659

5760
@Override
@@ -74,6 +77,11 @@ public Map<String, Object> getMessageParameters() {
7477
return messageParameters;
7578
}
7679

80+
@Override
81+
public boolean isExpressionLanguageEnabled() {
82+
return expressionLanguageEnabled;
83+
}
84+
7785
@Override
7886
public Map<String, Object> getExpressionVariables() {
7987
return expressionVariables;
@@ -135,6 +143,7 @@ public String toString() {
135143
sb.append( ", propertyPath=" ).append( propertyPath );
136144
sb.append( ", messageParameters=" ).append( messageParameters );
137145
sb.append( ", expressionVariables=" ).append( expressionVariables );
146+
sb.append( ", expressionLanguageEnabled=" ).append( expressionLanguageEnabled );
138147
sb.append( '}' );
139148
return sb.toString();
140149
}

engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/ConstraintValidatorContextImpl.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package org.hibernate.validator.internal.engine.constraintvalidation;
88

9+
import java.lang.annotation.Annotation;
910
import java.lang.invoke.MethodHandles;
1011
import java.util.ArrayList;
1112
import java.util.Collections;
@@ -28,6 +29,7 @@
2829
import javax.validation.metadata.ConstraintDescriptor;
2930

3031
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
32+
import org.hibernate.validator.constraintvalidation.HibernateConstraintViolationBuilder;
3133
import org.hibernate.validator.internal.engine.path.PathImpl;
3234
import org.hibernate.validator.internal.util.CollectionHelper;
3335
import org.hibernate.validator.internal.util.Contracts;
@@ -75,7 +77,7 @@ public final String getDefaultConstraintMessageTemplate() {
7577
}
7678

7779
@Override
78-
public ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate) {
80+
public HibernateConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate) {
7981
return new ConstraintViolationBuilderImpl(
8082
messageTemplate,
8183
getCopyOfBasePath()
@@ -168,6 +170,7 @@ protected final PathImpl getCopyOfBasePath() {
168170
private ConstraintViolationCreationContext getDefaultConstraintViolationCreationContext() {
169171
return new ConstraintViolationCreationContext(
170172
getDefaultConstraintMessageTemplate(),
173+
true, // EL is enabled for the default constraint violation
171174
basePath,
172175
messageParameters != null ? new HashMap<>( messageParameters ) : Collections.emptyMap(),
173176
expressionVariables != null ? new HashMap<>( expressionVariables ) : Collections.emptyMap(),
@@ -178,6 +181,7 @@ private ConstraintViolationCreationContext getDefaultConstraintViolationCreation
178181
private abstract class NodeBuilderBase {
179182

180183
protected final String messageTemplate;
184+
protected boolean expressionLanguageEnabled;
181185
protected PathImpl propertyPath;
182186

183187
protected NodeBuilderBase(String template, PathImpl path) {
@@ -189,9 +193,14 @@ public ConstraintValidatorContext addConstraintViolation() {
189193
if ( constraintViolationCreationContexts == null ) {
190194
constraintViolationCreationContexts = CollectionHelper.newArrayList( 3 );
191195
}
196+
if ( !(expressionVariables == null || expressionVariables.isEmpty()) && !expressionLanguageEnabled ) {
197+
LOG.expressionVariablesDefinedWithExpressionLanguageNotEnabled(
198+
constraintDescriptor.getAnnotation() != null ? constraintDescriptor.getAnnotation().annotationType() : Annotation.class );
199+
}
192200
constraintViolationCreationContexts.add(
193201
new ConstraintViolationCreationContext(
194202
messageTemplate,
203+
expressionLanguageEnabled,
195204
propertyPath,
196205
messageParameters != null ? new HashMap<>( messageParameters ) : Collections.emptyMap(),
197206
expressionVariables != null ? new HashMap<>( expressionVariables ) : Collections.emptyMap(),
@@ -202,12 +211,18 @@ public ConstraintValidatorContext addConstraintViolation() {
202211
}
203212
}
204213

205-
protected class ConstraintViolationBuilderImpl extends NodeBuilderBase implements ConstraintViolationBuilder {
214+
protected class ConstraintViolationBuilderImpl extends NodeBuilderBase implements HibernateConstraintViolationBuilder {
206215

207216
protected ConstraintViolationBuilderImpl(String template, PathImpl path) {
208217
super( template, path );
209218
}
210219

220+
@Override
221+
public HibernateConstraintViolationBuilder enableExpressionLanguage() {
222+
expressionLanguageEnabled = true;
223+
return this;
224+
}
225+
211226
@Override
212227
@Deprecated
213228
public NodeBuilderDefinedContext addNode(String name) {
@@ -221,12 +236,12 @@ public NodeBuilderDefinedContext addNode(String name) {
221236
public NodeBuilderCustomizableContext addPropertyNode(String name) {
222237
dropLeafNodeIfRequired();
223238

224-
return new DeferredNodeBuilder( messageTemplate, propertyPath, name, ElementKind.PROPERTY );
239+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, name, ElementKind.PROPERTY );
225240
}
226241

227242
@Override
228243
public LeafNodeBuilderCustomizableContext addBeanNode() {
229-
return new DeferredNodeBuilder( messageTemplate, propertyPath, null, ElementKind.BEAN );
244+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, null, ElementKind.BEAN );
230245
}
231246

232247
@Override
@@ -238,7 +253,7 @@ public NodeBuilderDefinedContext addParameterNode(int index) {
238253
public ContainerElementNodeBuilderCustomizableContext addContainerElementNode(String name, Class<?> containerType, Integer typeArgumentIndex) {
239254
dropLeafNodeIfRequired();
240255

241-
return new DeferredNodeBuilder( messageTemplate, propertyPath, name, containerType, typeArgumentIndex );
256+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, name, containerType, typeArgumentIndex );
242257
}
243258

244259
/**
@@ -267,17 +282,17 @@ public ConstraintViolationBuilder.NodeBuilderCustomizableContext addNode(String
267282

268283
@Override
269284
public NodeBuilderCustomizableContext addPropertyNode(String name) {
270-
return new DeferredNodeBuilder( messageTemplate, propertyPath, name, ElementKind.PROPERTY );
285+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, name, ElementKind.PROPERTY );
271286
}
272287

273288
@Override
274289
public LeafNodeBuilderCustomizableContext addBeanNode() {
275-
return new DeferredNodeBuilder( messageTemplate, propertyPath, null, ElementKind.BEAN );
290+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, null, ElementKind.BEAN );
276291
}
277292

278293
@Override
279294
public ContainerElementNodeBuilderCustomizableContext addContainerElementNode(String name, Class<?> containerType, Integer typeArgumentIndex) {
280-
return new DeferredNodeBuilder( messageTemplate, propertyPath, name, containerType, typeArgumentIndex );
295+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, name, containerType, typeArgumentIndex );
281296
}
282297
}
283298

@@ -293,16 +308,23 @@ private class DeferredNodeBuilder extends NodeBuilderBase
293308

294309
private final Integer leafNodeTypeArgumentIndex;
295310

296-
private DeferredNodeBuilder(String template, PathImpl path, String nodeName, ElementKind leafNodeKind) {
311+
private DeferredNodeBuilder(String template, boolean expressionLanguageEnabled, PathImpl path, String nodeName, ElementKind leafNodeKind) {
297312
super( template, path );
313+
this.expressionLanguageEnabled = expressionLanguageEnabled;
298314
this.leafNodeName = nodeName;
299315
this.leafNodeKind = leafNodeKind;
300316
this.leafNodeContainerType = null;
301317
this.leafNodeTypeArgumentIndex = null;
302318
}
303319

304-
private DeferredNodeBuilder(String template, PathImpl path, String nodeName, Class<?> leafNodeContainerType, Integer leafNodeTypeArgumentIndex) {
320+
private DeferredNodeBuilder(String template,
321+
boolean expressionLanguageEnabled,
322+
PathImpl path,
323+
String nodeName,
324+
Class<?> leafNodeContainerType,
325+
Integer leafNodeTypeArgumentIndex) {
305326
super( template, path );
327+
this.expressionLanguageEnabled = expressionLanguageEnabled;
306328
this.leafNodeName = nodeName;
307329
this.leafNodeKind = ElementKind.CONTAINER_ELEMENT;
308330
this.leafNodeContainerType = leafNodeContainerType;
@@ -344,19 +366,19 @@ public NodeBuilderCustomizableContext addNode(String name) {
344366
@Override
345367
public NodeBuilderCustomizableContext addPropertyNode(String name) {
346368
addLeafNode();
347-
return new DeferredNodeBuilder( messageTemplate, propertyPath, name, ElementKind.PROPERTY );
369+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, name, ElementKind.PROPERTY );
348370
}
349371

350372
@Override
351373
public ContainerElementNodeBuilderCustomizableContext addContainerElementNode(String name, Class<?> containerType, Integer typeArgumentIndex) {
352374
addLeafNode();
353-
return new DeferredNodeBuilder( messageTemplate, propertyPath, name, containerType, typeArgumentIndex );
375+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, name, containerType, typeArgumentIndex );
354376
}
355377

356378
@Override
357379
public LeafNodeBuilderCustomizableContext addBeanNode() {
358380
addLeafNode();
359-
return new DeferredNodeBuilder( messageTemplate, propertyPath, null, ElementKind.BEAN );
381+
return new DeferredNodeBuilder( messageTemplate, expressionLanguageEnabled, propertyPath, null, ElementKind.BEAN );
360382
}
361383

362384
@Override

0 commit comments

Comments
 (0)