Skip to content

Commit 2ce70d3

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, _]) Closes #1325
1 parent e78f7df commit 2ce70d3

File tree

6 files changed

+182
-0
lines changed

6 files changed

+182
-0
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
* @author Greg Turnquist
3838
* @author Florian Lüdiger
3939
* @author Bastian Wilhelm
40+
* @author Kurt Niemi
4041
*/
4142
public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty<RelationalPersistentProperty>
4243
implements RelationalPersistentProperty {
@@ -48,6 +49,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
4849
private final Lazy<String> embeddedPrefix;
4950
private final NamingStrategy namingStrategy;
5051
private boolean forceQuote = true;
52+
private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor();
5153

5254
/**
5355
* Creates a new {@link BasicRelationalPersistentProperty}.
@@ -90,6 +92,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
9092

9193
this.columnName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Column.class)) //
9294
.map(Column::value) //
95+
.map(spelExpressionProcessor::applySpelExpression) //
9396
.filter(StringUtils::hasText) //
9497
.map(this::createSqlIdentifier) //
9598
.orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))));

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

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* @author Greg Turnquist
3232
* @author Bastian Wilhelm
3333
* @author Mikhail Polivakha
34+
* @author Kurt Niemi
3435
*/
3536
class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, RelationalPersistentProperty>
3637
implements RelationalPersistentEntity<T> {
@@ -39,6 +40,7 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio
3940
private final Lazy<Optional<SqlIdentifier>> tableName;
4041
private final Lazy<Optional<SqlIdentifier>> schemaName;
4142
private boolean forceQuote = true;
43+
private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor();
4244

4345
/**
4446
* Creates a new {@link RelationalPersistentEntityImpl} for the given {@link TypeInformation}.
@@ -53,6 +55,7 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio
5355

5456
this.tableName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)) //
5557
.map(Table::value) //
58+
.map(spelExpressionProcessor::applySpelExpression) //
5659
.filter(StringUtils::hasText) //
5760
.map(this::createSqlIdentifier));
5861

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.springframework.data.relational.core.mapping;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.expression.EvaluationException;
5+
import org.springframework.expression.Expression;
6+
import org.springframework.expression.common.TemplateParserContext;
7+
import org.springframework.expression.spel.standard.SpelExpressionParser;
8+
import org.springframework.expression.spel.support.StandardEvaluationContext;
9+
import org.springframework.util.Assert;
10+
11+
/**
12+
* Provide support for processing SpEL expressions in @Table and @Column annotations,
13+
* or anywhere we want to use SpEL expressions and sanitize the result of the evaluated
14+
* SpEL expression.
15+
*
16+
* The default sanitization allows for digits, alphabetic characters and _ characters
17+
* and strips out any other characters.
18+
*
19+
* Custom sanitization (if desired) can be achieved by creating a class that implements
20+
* the {@link #SpelExpressionResultSanitizer} interface and configuring a spelExpressionResultSanitizer
21+
* Bean.
22+
*
23+
* @author Kurt Niemi
24+
* @since 3.1
25+
*/
26+
public class SpelExpressionProcessor {
27+
@Autowired(required = false)
28+
private SpelExpressionResultSanitizer spelExpressionResultSanitizer;
29+
private StandardEvaluationContext evalContext = new StandardEvaluationContext();
30+
private SpelExpressionParser parser = new SpelExpressionParser();
31+
private TemplateParserContext templateParserContext = new TemplateParserContext();
32+
33+
public String applySpelExpression(String expression) throws EvaluationException {
34+
35+
Assert.notNull(expression, "Expression must not be null.");
36+
37+
// Only apply logic if we have the prefixes/suffixes required for a SpEL expression as firstly
38+
// there is nothing to evaluate (i.e. whatever literal passed in is returned as-is) and more
39+
// importantly we do not want to perform any sanitization logic.
40+
if (!isSpellExpression(expression)) {
41+
return expression;
42+
}
43+
44+
Expression expr = parser.parseExpression(expression, templateParserContext);
45+
String result = expr.getValue(evalContext, String.class);
46+
47+
// Normally an exception is thrown by the Spel parser on invalid syntax/errors but this will provide
48+
// a consistent experience for any issues with Spel parsing.
49+
if (result == null) {
50+
throw new EvaluationException("Spel Parsing of expression \"" + expression + "\" failed.");
51+
}
52+
53+
String sanitizedResult = getSpelExpressionResultSanitizer().sanitize(result);
54+
55+
return sanitizedResult;
56+
}
57+
58+
protected boolean isSpellExpression(String expression) {
59+
60+
String trimmedExpression = expression.trim();
61+
if (trimmedExpression.startsWith(templateParserContext.getExpressionPrefix()) &&
62+
trimmedExpression.endsWith(templateParserContext.getExpressionSuffix())) {
63+
return true;
64+
}
65+
66+
return false;
67+
}
68+
private SpelExpressionResultSanitizer getSpelExpressionResultSanitizer() {
69+
70+
if (this.spelExpressionResultSanitizer == null) {
71+
this.spelExpressionResultSanitizer = new SpelExpressionResultSanitizer() {
72+
@Override
73+
public String sanitize(String result) {
74+
75+
String cleansedResult = result.replaceAll("[^\\w]", "");
76+
return cleansedResult;
77+
}
78+
};
79+
}
80+
return this.spelExpressionResultSanitizer;
81+
}
82+
}
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+
}

spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java

+30
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
* @author Oliver Gierke
4141
* @author Florian Lüdiger
4242
* @author Bastian Wilhelm
43+
* @author Kurt Niemi
4344
*/
4445
public class BasicRelationalPersistentPropertyUnitTests {
4546

@@ -68,6 +69,19 @@ public void detectsAnnotatedColumnAndKeyName() {
6869
assertThat(listProperty.getKeyColumn()).isEqualTo(quoted("dummy_key_column_name"));
6970
}
7071

72+
@Test // GH-1325
73+
void testRelationalPersistentEntitySpelExpressions() {
74+
75+
assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName()).isEqualTo(quoted("THE_FORCE_IS_WITH_YOU"));
76+
assertThat(entity.getRequiredPersistentProperty("littleBobbyTables").getColumnName())
77+
.isEqualTo(quoted("DROPALLTABLES"));
78+
79+
// Test that sanitizer does affect non-spel expressions
80+
assertThat(entity.getRequiredPersistentProperty(
81+
"poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot").getColumnName())
82+
.isEqualTo(quoted("--; DROP ALL TABLES;--"));
83+
}
84+
7185
@Test // DATAJDBC-111
7286
public void detectsEmbeddedEntity() {
7387

@@ -149,6 +163,22 @@ private static class DummyEntity {
149163
// DATACMNS-106
150164
private @Column("dummy_name") String name;
151165

166+
public static String spelExpression1Value = "THE_FORCE_IS_WITH_YOU";
167+
168+
public static String littleBobbyTablesValue = "--; DROP ALL TABLES;--";
169+
@Column(value="#{T(org.springframework.data.relational.core.mapping." +
170+
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" +
171+
").spelExpression1Value}")
172+
private String spelExpression1;
173+
174+
@Column(value="#{T(org.springframework.data.relational.core.mapping." +
175+
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" +
176+
").littleBobbyTablesValue}")
177+
private String littleBobbyTables;
178+
179+
@Column(value="--; DROP ALL TABLES;--")
180+
private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot;
181+
152182
// DATAJDBC-111
153183
private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity;
154184

spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java

+54
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* @author Bastian Wilhelm
3232
* @author Mark Paluch
3333
* @author Mikhail Polivakha
34+
* @author Kurt Niemi
3435
*/
3536
class RelationalPersistentEntityImplUnitTests {
3637

@@ -96,6 +97,41 @@ void testRelationalPersistentEntitySchemaNameChoice() {
9697
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
9798
}
9899

100+
@Test // GH-1325
101+
void testRelationalPersistentEntitySpelExpression() {
102+
103+
mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE);
104+
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndTableSpelExpression.class);
105+
106+
SqlIdentifier simpleExpected = quoted("USE_THE_FORCE");
107+
SqlIdentifier expected = SqlIdentifier.from(quoted("HELP_ME_OBI_WON"), simpleExpected);
108+
assertThat(entity.getQualifiedTableName()).isEqualTo(expected);
109+
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
110+
}
111+
@Test // GH-1325
112+
void testRelationalPersistentEntitySpelExpression_Sanitized() {
113+
114+
mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE);
115+
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(LittleBobbyTables.class);
116+
117+
SqlIdentifier simpleExpected = quoted("RobertDROPTABLEstudents");
118+
SqlIdentifier expected = SqlIdentifier.from(quoted("LITTLE_BOBBY_TABLES"), simpleExpected);
119+
assertThat(entity.getQualifiedTableName()).isEqualTo(expected);
120+
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
121+
}
122+
123+
@Test // GH-1325
124+
void testRelationalPersistentEntitySpelExpression_NonSpelExpression() {
125+
126+
mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE);
127+
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndName.class);
128+
129+
SqlIdentifier simpleExpected = quoted("I_AM_THE_SENATE");
130+
SqlIdentifier expected = SqlIdentifier.from(quoted("DART_VADER"), simpleExpected);
131+
assertThat(entity.getQualifiedTableName()).isEqualTo(expected);
132+
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
133+
}
134+
99135
@Test // GH-1099
100136
void specifiedSchemaGetsCombinedWithNameFromNamingStrategy() {
101137

@@ -117,6 +153,24 @@ private static class EntityWithSchemaAndName {
117153
@Id private Long id;
118154
}
119155

156+
@Table(schema = "HELP_ME_OBI_WON",
157+
name="#{T(org.springframework.data.relational.core.mapping." +
158+
"RelationalPersistentEntityImplUnitTests$EntityWithSchemaAndTableSpelExpression" +
159+
").desiredTableName}")
160+
private static class EntityWithSchemaAndTableSpelExpression {
161+
@Id private Long id;
162+
public static String desiredTableName = "USE_THE_FORCE";
163+
}
164+
165+
@Table(schema = "LITTLE_BOBBY_TABLES",
166+
name="#{T(org.springframework.data.relational.core.mapping." +
167+
"RelationalPersistentEntityImplUnitTests$LittleBobbyTables" +
168+
").desiredTableName}")
169+
private static class LittleBobbyTables {
170+
@Id private Long id;
171+
public static String desiredTableName = "Robert'); DROP TABLE students;--";
172+
}
173+
120174
@Table("dummy_sub_entity")
121175
static class DummySubEntity {
122176
@Id @Column("renamedId") Long id;

0 commit comments

Comments
 (0)