Skip to content

Commit 2e52216

Browse files
committed
Add SpEl support for table and column names
If SpEl expressions are specified in the @table or @column annotation, they will be evaluated and the output will be sanitized to prevent SQL Injections. The default sanitization only allows digits, alphabetic characters, and _ character. (i.e. [0-9, a-z, A-Z, _]) It is possible to customize the sanitization by implementing a class that implements the SpelExpressionResultSanitizer interface by configuring the spellExpressionResultSanitizer bean Closes #1325
1 parent 2305dc3 commit 2e52216

File tree

8 files changed

+261
-16
lines changed

8 files changed

+261
-16
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/BasicJdbcPersistentProperty.java

+10-7
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,14 @@
1818
import org.springframework.data.mapping.PersistentEntity;
1919
import org.springframework.data.mapping.model.Property;
2020
import org.springframework.data.mapping.model.SimpleTypeHolder;
21-
import org.springframework.data.relational.core.mapping.BasicRelationalPersistentProperty;
22-
import org.springframework.data.relational.core.mapping.NamingStrategy;
23-
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
24-
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
21+
import org.springframework.data.relational.core.mapping.*;
2522

2623
/**
2724
* Extension to {@link BasicRelationalPersistentProperty}.
2825
*
2926
* @author Mark Paluch
3027
* @author Jens Schauder
28+
* @author Kurt Niemi
3129
*/
3230
public class BasicJdbcPersistentProperty extends BasicRelationalPersistentProperty {
3331

@@ -38,11 +36,16 @@ public class BasicJdbcPersistentProperty extends BasicRelationalPersistentProper
3836
* @param owner must not be {@literal null}.
3937
* @param simpleTypeHolder must not be {@literal null}.
4038
* @param namingStrategy must not be {@literal null}
39+
* @param sanitizer must not be {@literal null}
4140
* @since 2.0
4241
*/
43-
public BasicJdbcPersistentProperty(Property property, PersistentEntity<?, RelationalPersistentProperty> owner,
44-
SimpleTypeHolder simpleTypeHolder, NamingStrategy namingStrategy) {
45-
super(property, owner, simpleTypeHolder, namingStrategy);
42+
public BasicJdbcPersistentProperty(Property property,
43+
PersistentEntity<?, RelationalPersistentProperty> owner,
44+
SimpleTypeHolder simpleTypeHolder,
45+
NamingStrategy namingStrategy,
46+
SpelExpressionResultSanitizer sanitizer) {
47+
48+
super(property, owner, simpleTypeHolder, namingStrategy, sanitizer);
4649
}
4750

4851
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ protected <T> RelationalPersistentEntity<T> createPersistentEntity(TypeInformati
8080
protected RelationalPersistentProperty createPersistentProperty(Property property,
8181
RelationalPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
8282
BasicJdbcPersistentProperty persistentProperty = new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder,
83-
this.getNamingStrategy());
83+
this.getNamingStrategy(),super.getSpelExpressionResultSanitizer());
8484
persistentProperty.setForceQuote(isForceQuote());
8585
return persistentProperty;
8686
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java

+77-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
import org.springframework.data.relational.core.sql.SqlIdentifier;
2828
import org.springframework.data.util.Lazy;
2929
import org.springframework.data.util.Optionals;
30+
import org.springframework.expression.EvaluationException;
31+
import org.springframework.expression.Expression;
32+
import org.springframework.expression.common.TemplateParserContext;
33+
import org.springframework.expression.spel.standard.SpelExpressionParser;
34+
import org.springframework.expression.spel.support.StandardEvaluationContext;
3035
import org.springframework.util.Assert;
3136
import org.springframework.util.StringUtils;
3237

@@ -37,6 +42,7 @@
3742
* @author Greg Turnquist
3843
* @author Florian Lüdiger
3944
* @author Bastian Wilhelm
45+
* @author Kurt Niemi
4046
*/
4147
public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty<RelationalPersistentProperty>
4248
implements RelationalPersistentProperty {
@@ -48,6 +54,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
4854
private final Lazy<String> embeddedPrefix;
4955
private final NamingStrategy namingStrategy;
5056
private boolean forceQuote = true;
57+
private SpelExpressionResultSanitizer sanitizer;
5158

5259
/**
5360
* Creates a new {@link BasicRelationalPersistentProperty}.
@@ -57,12 +64,12 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
5764
* @param simpleTypeHolder must not be {@literal null}.
5865
* @param context must not be {@literal null}
5966
* @since 2.0, use
60-
* {@link #BasicRelationalPersistentProperty(Property, PersistentEntity, SimpleTypeHolder, NamingStrategy)}.
67+
* {@link #BasicRelationalPersistentProperty(Property, PersistentEntity, SimpleTypeHolder, NamingStrategy, SpelExpressionResultSanitizer)}.
6168
*/
62-
@Deprecated
69+
@Deprecated(since = "3.1", forRemoval = true)
6370
public BasicRelationalPersistentProperty(Property property, PersistentEntity<?, RelationalPersistentProperty> owner,
6471
SimpleTypeHolder simpleTypeHolder, RelationalMappingContext context) {
65-
this(property, owner, simpleTypeHolder, context.getNamingStrategy());
72+
this(property, owner, simpleTypeHolder, context.getNamingStrategy(), context.getSpelExpressionResultSanitizer());
6673
}
6774

6875
/**
@@ -74,11 +81,34 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
7481
* @param namingStrategy must not be {@literal null}
7582
* @since 2.0
7683
*/
77-
public BasicRelationalPersistentProperty(Property property, PersistentEntity<?, RelationalPersistentProperty> owner,
78-
SimpleTypeHolder simpleTypeHolder, NamingStrategy namingStrategy) {
84+
@Deprecated(since = "3.1", forRemoval = true)
85+
public BasicRelationalPersistentProperty(Property property,
86+
PersistentEntity<?, RelationalPersistentProperty> owner,
87+
SimpleTypeHolder simpleTypeHolder,
88+
NamingStrategy namingStrategy) {
89+
// ::TODO:: Where should I put the code that has the default sanitizer implementation??
90+
this(property, owner, simpleTypeHolder, namingStrategy, null);
91+
}
92+
93+
/**
94+
* Creates a new {@link BasicRelationalPersistentProperty}.
95+
*
96+
* @param property must not be {@literal null}.
97+
* @param owner must not be {@literal null}.
98+
* @param simpleTypeHolder must not be {@literal null}.
99+
* @param namingStrategy must not be {@literal null}
100+
* @param sanitizer must not be {@literal null}
101+
* @since 2.0
102+
*/
103+
public BasicRelationalPersistentProperty(Property property,
104+
PersistentEntity<?, RelationalPersistentProperty> owner,
105+
SimpleTypeHolder simpleTypeHolder,
106+
NamingStrategy namingStrategy,
107+
SpelExpressionResultSanitizer sanitizer) {
79108

80109
super(property, owner, simpleTypeHolder);
81110
this.namingStrategy = namingStrategy;
111+
this.sanitizer = sanitizer;
82112

83113
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
84114

@@ -90,6 +120,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
90120

91121
this.columnName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Column.class)) //
92122
.map(Column::value) //
123+
.map(this::applySpelExpression)
93124
.filter(StringUtils::hasText) //
94125
.map(this::createSqlIdentifier) //
95126
.orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))));
@@ -110,6 +141,47 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
110141
.orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this))));
111142
}
112143

144+
// ::TODO:: Discuss where we want this common code for Table and Columns
145+
private StandardEvaluationContext evalContext = new StandardEvaluationContext();
146+
private SpelExpressionParser parser = new SpelExpressionParser();
147+
private TemplateParserContext templateParserContext = new TemplateParserContext();
148+
private boolean isSpellExpression(String expression) {
149+
150+
String trimmedExpression = expression.trim();
151+
if (trimmedExpression.startsWith(templateParserContext.getExpressionPrefix()) &&
152+
trimmedExpression.endsWith(templateParserContext.getExpressionSuffix())) {
153+
return true;
154+
}
155+
156+
return false;
157+
}
158+
159+
private String applySpelExpression(String expression) throws EvaluationException {
160+
161+
Assert.notNull(expression, "Expression must not be null.");
162+
163+
// Only apply logic if we have the prefixes/suffixes required for a Spel expression as firstly
164+
// there is nothing to evaluate (i.e. whatever literal passed in is returned as-is) and more
165+
// importantly we do not want to perform any sanitization logic.
166+
if (!isSpellExpression(expression)) {
167+
return expression;
168+
}
169+
170+
Expression expr = parser.parseExpression(expression, templateParserContext);
171+
String result = expr.getValue(evalContext, String.class);
172+
173+
// Normally an exception is thrown by the Spel parser on invalid syntax/errors but this will provide
174+
// a consistent experience for any issues with Spel parsing.
175+
if (result == null) {
176+
throw new EvaluationException("Spel Parsing of expression \"" + expression + "\" failed.");
177+
}
178+
179+
String sanitizedResult = sanitizer.sanitize(result);
180+
181+
return sanitizedResult;
182+
}
183+
184+
113185
private SqlIdentifier createSqlIdentifier(String name) {
114186
return isForceQuote() ? SqlIdentifier.quoted(name) : SqlIdentifier.unquoted(name);
115187
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.relational.core.mapping;
1717

18+
import org.springframework.beans.factory.annotation.Autowired;
1819
import org.springframework.data.mapping.context.AbstractMappingContext;
1920
import org.springframework.data.mapping.context.MappingContext;
2021
import org.springframework.data.mapping.model.Property;
@@ -30,13 +31,17 @@
3031
* @author Kazuki Shimizu
3132
* @author Oliver Gierke
3233
* @author Mark Paluch
34+
* @author Kurt Niemi
3335
*/
3436
public class RelationalMappingContext
3537
extends AbstractMappingContext<RelationalPersistentEntity<?>, RelationalPersistentProperty> {
3638

3739
private final NamingStrategy namingStrategy;
3840
private boolean forceQuote = true;
3941

42+
@Autowired(required = false)
43+
private SpelExpressionResultSanitizer spelExpressionResultSanitizer;
44+
4045
/**
4146
* Creates a new {@link RelationalMappingContext}.
4247
*/
@@ -77,11 +82,27 @@ public void setForceQuote(boolean forceQuote) {
7782
this.forceQuote = forceQuote;
7883
}
7984

85+
protected SpelExpressionResultSanitizer getSpelExpressionResultSanitizer() {
86+
87+
if (this.spelExpressionResultSanitizer == null) {
88+
this.spelExpressionResultSanitizer = new SpelExpressionResultSanitizer() {
89+
@Override
90+
public String sanitize(String result) {
91+
92+
String cleansedResult = result.replaceAll("[^\\w]", "");
93+
return cleansedResult;
94+
}
95+
};
96+
}
97+
return this.spelExpressionResultSanitizer;
98+
}
99+
80100
@Override
81101
protected <T> RelationalPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
82102

83103
RelationalPersistentEntityImpl<T> entity = new RelationalPersistentEntityImpl<>(typeInformation,
84-
this.namingStrategy);
104+
this.namingStrategy,
105+
getSpelExpressionResultSanitizer());
85106
entity.setForceQuote(isForceQuote());
86107

87108
return entity;
@@ -92,7 +113,7 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert
92113
RelationalPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
93114

94115
BasicRelationalPersistentProperty persistentProperty = new BasicRelationalPersistentProperty(property, owner,
95-
simpleTypeHolder, this.namingStrategy);
116+
simpleTypeHolder, this.namingStrategy, getSpelExpressionResultSanitizer());
96117
persistentProperty.setForceQuote(isForceQuote());
97118

98119
return persistentProperty;

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java

+56-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,16 @@
2121
import org.springframework.data.relational.core.sql.SqlIdentifier;
2222
import org.springframework.data.util.Lazy;
2323
import org.springframework.data.util.TypeInformation;
24+
import org.springframework.expression.EvaluationException;
25+
import org.springframework.expression.Expression;
26+
import org.springframework.expression.ParserContext;
27+
import org.springframework.expression.common.TemplateParserContext;
2428
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
2530
import org.springframework.util.StringUtils;
31+
import org.springframework.expression.spel.standard.SpelExpressionParser;
32+
import org.springframework.expression.spel.support.StandardEvaluationContext;
33+
2634

2735
/**
2836
* Meta data a repository might need for implementing persistence operations for instances of type {@code T}
@@ -31,6 +39,7 @@
3139
* @author Greg Turnquist
3240
* @author Bastian Wilhelm
3341
* @author Mikhail Polivakha
42+
* @author Kurt Niemi
3443
*/
3544
class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, RelationalPersistentProperty>
3645
implements RelationalPersistentEntity<T> {
@@ -40,19 +49,29 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio
4049
private final Lazy<Optional<SqlIdentifier>> schemaName;
4150
private boolean forceQuote = true;
4251

52+
private SpelExpressionResultSanitizer sanitizer;
53+
private StandardEvaluationContext evalContext = new StandardEvaluationContext();
54+
private SpelExpressionParser parser = new SpelExpressionParser();
55+
private TemplateParserContext templateParserContext = new TemplateParserContext();
56+
57+
4358
/**
4459
* Creates a new {@link RelationalPersistentEntityImpl} for the given {@link TypeInformation}.
4560
*
4661
* @param information must not be {@literal null}.
4762
*/
48-
RelationalPersistentEntityImpl(TypeInformation<T> information, NamingStrategy namingStrategy) {
63+
RelationalPersistentEntityImpl(TypeInformation<T> information,
64+
NamingStrategy namingStrategy,
65+
SpelExpressionResultSanitizer sanitizer) {
4966

5067
super(information);
5168

5269
this.namingStrategy = namingStrategy;
70+
this.sanitizer = sanitizer;
5371

5472
this.tableName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)) //
5573
.map(Table::value) //
74+
.map(this::applySpelExpression)
5675
.filter(StringUtils::hasText) //
5776
.map(this::createSqlIdentifier));
5877

@@ -66,6 +85,42 @@ private SqlIdentifier createSqlIdentifier(String name) {
6685
return isForceQuote() ? SqlIdentifier.quoted(name) : SqlIdentifier.unquoted(name);
6786
}
6887

88+
private boolean isSpellExpression(String expression) {
89+
90+
String trimmedExpression = expression.trim();
91+
if (trimmedExpression.startsWith(templateParserContext.getExpressionPrefix()) &&
92+
trimmedExpression.endsWith(templateParserContext.getExpressionSuffix())) {
93+
return true;
94+
}
95+
96+
return false;
97+
}
98+
99+
private String applySpelExpression(String expression) throws EvaluationException {
100+
101+
Assert.notNull(expression, "Expression must not be null.");
102+
103+
// Only apply logic if we have the prefixes/suffixes required for a Spel expression as firstly
104+
// there is nothing to evaluate (i.e. whatever literal passed in is returned as-is) and more
105+
// importantly we do not want to perform any sanitization logic.
106+
if (!isSpellExpression(expression)) {
107+
return expression;
108+
}
109+
110+
Expression expr = parser.parseExpression(expression, templateParserContext);
111+
String result = expr.getValue(evalContext, String.class);
112+
113+
// Normally an exception is thrown by the Spel parser on invalid syntax/errors but this will provide
114+
// a consistent experience for any issues with Spel parsing.
115+
if (result == null) {
116+
throw new EvaluationException("Spel Parsing of expression \"" + expression + "\" failed.");
117+
}
118+
119+
String sanitizedResult = sanitizer.sanitize(result);
120+
121+
return sanitizedResult;
122+
}
123+
69124
private SqlIdentifier createDerivedSqlIdentifier(String name) {
70125
return new DerivedSqlIdentifier(name, isForceQuote());
71126
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.springframework.data.relational.core.mapping;
2+
3+
/**
4+
* Interface for sanitizing Spel Expression results
5+
*
6+
* @author Kurt Niemi
7+
*/
8+
public interface SpelExpressionResultSanitizer {
9+
public String sanitize(String result);
10+
}

0 commit comments

Comments
 (0)