Browse Source

Add SpEL support for `@Table` and `@Column`.

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
Original pull request: #1461
pull/1526/head
Kurt Niemi 3 years ago committed by Mark Paluch
parent
commit
68a13fe12b
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 1
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java
  2. 10
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java
  3. 11
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java
  4. 11
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java
  5. 87
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionProcessor.java
  6. 10
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionResultSanitizer.java
  7. 30
      spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java
  8. 54
      spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java

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

@ -82,6 +82,7 @@ public class JdbcMappingContext extends RelationalMappingContext { @@ -82,6 +82,7 @@ public class JdbcMappingContext extends RelationalMappingContext {
BasicJdbcPersistentProperty persistentProperty = new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder,
this.getNamingStrategy());
persistentProperty.setForceQuote(isForceQuote());
persistentProperty.setSpelExpressionProcessor(getSpelExpressionProcessor());
return persistentProperty;
}

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

@ -37,6 +37,7 @@ import org.springframework.util.StringUtils; @@ -37,6 +37,7 @@ import org.springframework.util.StringUtils;
* @author Greg Turnquist
* @author Florian Lüdiger
* @author Bastian Wilhelm
* @author Kurt Niemi
*/
public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty<RelationalPersistentProperty>
implements RelationalPersistentProperty {
@ -48,6 +49,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent @@ -48,6 +49,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
private final Lazy<String> embeddedPrefix;
private final NamingStrategy namingStrategy;
private boolean forceQuote = true;
private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor();
/**
* Creates a new {@link BasicRelationalPersistentProperty}.
@ -90,6 +92,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent @@ -90,6 +92,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
this.columnName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Column.class)) //
.map(Column::value) //
.map(spelExpressionProcessor::applySpelExpression) //
.filter(StringUtils::hasText) //
.map(this::createSqlIdentifier) //
.orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))));
@ -109,6 +112,13 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent @@ -109,6 +112,13 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
.map(this::createSqlIdentifier) //
.orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this))));
}
public SpelExpressionProcessor getSpelExpressionProcessor() {
return spelExpressionProcessor;
}
public void setSpelExpressionProcessor(SpelExpressionProcessor spelExpressionProcessor) {
this.spelExpressionProcessor = spelExpressionProcessor;
}
private SqlIdentifier createSqlIdentifier(String name) {
return isForceQuote() ? SqlIdentifier.quoted(name) : SqlIdentifier.unquoted(name);

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

@ -36,6 +36,7 @@ public class RelationalMappingContext @@ -36,6 +36,7 @@ public class RelationalMappingContext
private final NamingStrategy namingStrategy;
private boolean forceQuote = true;
private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor();
/**
* Creates a new {@link RelationalMappingContext}.
@ -77,12 +78,21 @@ public class RelationalMappingContext @@ -77,12 +78,21 @@ public class RelationalMappingContext
this.forceQuote = forceQuote;
}
public SpelExpressionProcessor getSpelExpressionProcessor() {
return spelExpressionProcessor;
}
public void setSpelExpressionProcessor(SpelExpressionProcessor spelExpressionProcessor) {
this.spelExpressionProcessor = spelExpressionProcessor;
}
@Override
protected <T> RelationalPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
RelationalPersistentEntityImpl<T> entity = new RelationalPersistentEntityImpl<>(typeInformation,
this.namingStrategy);
entity.setForceQuote(isForceQuote());
entity.setSpelExpressionProcessor(getSpelExpressionProcessor());
return entity;
}
@ -94,6 +104,7 @@ public class RelationalMappingContext @@ -94,6 +104,7 @@ public class RelationalMappingContext
BasicRelationalPersistentProperty persistentProperty = new BasicRelationalPersistentProperty(property, owner,
simpleTypeHolder, this.namingStrategy);
persistentProperty.setForceQuote(isForceQuote());
persistentProperty.setSpelExpressionProcessor(getSpelExpressionProcessor());
return persistentProperty;
}

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

@ -31,6 +31,7 @@ import org.springframework.util.StringUtils; @@ -31,6 +31,7 @@ import org.springframework.util.StringUtils;
* @author Greg Turnquist
* @author Bastian Wilhelm
* @author Mikhail Polivakha
* @author Kurt Niemi
*/
class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, RelationalPersistentProperty>
implements RelationalPersistentEntity<T> {
@ -39,6 +40,7 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio @@ -39,6 +40,7 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio
private final Lazy<Optional<SqlIdentifier>> tableName;
private final Lazy<Optional<SqlIdentifier>> schemaName;
private boolean forceQuote = true;
private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor();
/**
* Creates a new {@link RelationalPersistentEntityImpl} for the given {@link TypeInformation}.
@ -53,6 +55,7 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio @@ -53,6 +55,7 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio
this.tableName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)) //
.map(Table::value) //
.map(spelExpressionProcessor::applySpelExpression) //
.filter(StringUtils::hasText) //
.map(this::createSqlIdentifier));
@ -62,6 +65,14 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio @@ -62,6 +65,14 @@ class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, Relatio
.map(this::createSqlIdentifier));
}
public SpelExpressionProcessor getSpelExpressionProcessor() {
return spelExpressionProcessor;
}
public void setSpelExpressionProcessor(SpelExpressionProcessor spelExpressionProcessor) {
this.spelExpressionProcessor = spelExpressionProcessor;
}
private SqlIdentifier createSqlIdentifier(String name) {
return isForceQuote() ? SqlIdentifier.quoted(name) : SqlIdentifier.unquoted(name);
}

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

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
package org.springframework.data.relational.core.mapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
/**
* Provide support for processing SpEL expressions in @Table and @Column annotations,
* or anywhere we want to use SpEL expressions and sanitize the result of the evaluated
* SpEL expression.
*
* The default sanitization allows for digits, alphabetic characters and _ characters
* and strips out any other characters.
*
* Custom sanitization (if desired) can be achieved by creating a class that implements
* the {@link SpelExpressionResultSanitizer} interface and then invoking the
* {@link #setSpelExpressionResultSanitizer(SpelExpressionResultSanitizer)} method.
*
* @author Kurt Niemi
* @see SpelExpressionResultSanitizer
* @since 3.1
*/
public class SpelExpressionProcessor {
private SpelExpressionResultSanitizer spelExpressionResultSanitizer;
private StandardEvaluationContext evalContext = new StandardEvaluationContext();
private SpelExpressionParser parser = new SpelExpressionParser();
private TemplateParserContext templateParserContext = new TemplateParserContext();
public String applySpelExpression(String expression) throws EvaluationException {
Assert.notNull(expression, "Expression must not be null.");
// Only apply logic if we have the prefixes/suffixes required for a SpEL expression as firstly
// there is nothing to evaluate (i.e. whatever literal passed in is returned as-is) and more
// importantly we do not want to perform any sanitization logic.
if (!isSpellExpression(expression)) {
return expression;
}
Expression expr = parser.parseExpression(expression, templateParserContext);
String result = expr.getValue(evalContext, String.class);
// Normally an exception is thrown by the Spel parser on invalid syntax/errors but this will provide
// a consistent experience for any issues with Spel parsing.
if (result == null) {
throw new EvaluationException("Spel Parsing of expression \"" + expression + "\" failed.");
}
String sanitizedResult = getSpelExpressionResultSanitizer().sanitize(result);
return sanitizedResult;
}
protected boolean isSpellExpression(String expression) {
String trimmedExpression = expression.trim();
if (trimmedExpression.startsWith(templateParserContext.getExpressionPrefix()) &&
trimmedExpression.endsWith(templateParserContext.getExpressionSuffix())) {
return true;
}
return false;
}
public SpelExpressionResultSanitizer getSpelExpressionResultSanitizer() {
if (this.spelExpressionResultSanitizer == null) {
this.spelExpressionResultSanitizer = new SpelExpressionResultSanitizer() {
@Override
public String sanitize(String result) {
String cleansedResult = result.replaceAll("[^\\w]", "");
return cleansedResult;
}
};
}
return this.spelExpressionResultSanitizer;
}
public void setSpelExpressionResultSanitizer(SpelExpressionResultSanitizer spelExpressionResultSanitizer) {
this.spelExpressionResultSanitizer = spelExpressionResultSanitizer;
}
}

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

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
package org.springframework.data.relational.core.mapping;
/**
* Interface for sanitizing Spel Expression results
*
* @author Kurt Niemi
*/
public interface SpelExpressionResultSanitizer {
public String sanitize(String result);
}

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

@ -40,6 +40,7 @@ import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; @@ -40,6 +40,7 @@ import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
* @author Oliver Gierke
* @author Florian Lüdiger
* @author Bastian Wilhelm
* @author Kurt Niemi
*/
public class BasicRelationalPersistentPropertyUnitTests {
@ -68,6 +69,19 @@ public class BasicRelationalPersistentPropertyUnitTests { @@ -68,6 +69,19 @@ public class BasicRelationalPersistentPropertyUnitTests {
assertThat(listProperty.getKeyColumn()).isEqualTo(quoted("dummy_key_column_name"));
}
@Test // GH-1325
void testRelationalPersistentEntitySpelExpressions() {
assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName()).isEqualTo(quoted("THE_FORCE_IS_WITH_YOU"));
assertThat(entity.getRequiredPersistentProperty("littleBobbyTables").getColumnName())
.isEqualTo(quoted("DROPALLTABLES"));
// Test that sanitizer does affect non-spel expressions
assertThat(entity.getRequiredPersistentProperty(
"poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot").getColumnName())
.isEqualTo(quoted("--; DROP ALL TABLES;--"));
}
@Test // DATAJDBC-111
public void detectsEmbeddedEntity() {
@ -149,6 +163,22 @@ public class BasicRelationalPersistentPropertyUnitTests { @@ -149,6 +163,22 @@ public class BasicRelationalPersistentPropertyUnitTests {
// DATACMNS-106
private @Column("dummy_name") String name;
public static String spelExpression1Value = "THE_FORCE_IS_WITH_YOU";
public static String littleBobbyTablesValue = "--; DROP ALL TABLES;--";
@Column(value="#{T(org.springframework.data.relational.core.mapping." +
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" +
").spelExpression1Value}")
private String spelExpression1;
@Column(value="#{T(org.springframework.data.relational.core.mapping." +
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" +
").littleBobbyTablesValue}")
private String littleBobbyTables;
@Column(value="--; DROP ALL TABLES;--")
private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot;
// DATAJDBC-111
private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity;

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

@ -31,6 +31,7 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -31,6 +31,7 @@ import org.springframework.data.relational.core.sql.SqlIdentifier;
* @author Bastian Wilhelm
* @author Mark Paluch
* @author Mikhail Polivakha
* @author Kurt Niemi
*/
class RelationalPersistentEntityImplUnitTests {
@ -96,6 +97,41 @@ class RelationalPersistentEntityImplUnitTests { @@ -96,6 +97,41 @@ class RelationalPersistentEntityImplUnitTests {
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
}
@Test // GH-1325
void testRelationalPersistentEntitySpelExpression() {
mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE);
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndTableSpelExpression.class);
SqlIdentifier simpleExpected = quoted("USE_THE_FORCE");
SqlIdentifier expected = SqlIdentifier.from(quoted("HELP_ME_OBI_WON"), simpleExpected);
assertThat(entity.getQualifiedTableName()).isEqualTo(expected);
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
}
@Test // GH-1325
void testRelationalPersistentEntitySpelExpression_Sanitized() {
mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE);
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(LittleBobbyTables.class);
SqlIdentifier simpleExpected = quoted("RobertDROPTABLEstudents");
SqlIdentifier expected = SqlIdentifier.from(quoted("LITTLE_BOBBY_TABLES"), simpleExpected);
assertThat(entity.getQualifiedTableName()).isEqualTo(expected);
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
}
@Test // GH-1325
void testRelationalPersistentEntitySpelExpression_NonSpelExpression() {
mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE);
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndName.class);
SqlIdentifier simpleExpected = quoted("I_AM_THE_SENATE");
SqlIdentifier expected = SqlIdentifier.from(quoted("DART_VADER"), simpleExpected);
assertThat(entity.getQualifiedTableName()).isEqualTo(expected);
assertThat(entity.getTableName()).isEqualTo(simpleExpected);
}
@Test // GH-1099
void specifiedSchemaGetsCombinedWithNameFromNamingStrategy() {
@ -117,6 +153,24 @@ class RelationalPersistentEntityImplUnitTests { @@ -117,6 +153,24 @@ class RelationalPersistentEntityImplUnitTests {
@Id private Long id;
}
@Table(schema = "HELP_ME_OBI_WON",
name="#{T(org.springframework.data.relational.core.mapping." +
"RelationalPersistentEntityImplUnitTests$EntityWithSchemaAndTableSpelExpression" +
").desiredTableName}")
private static class EntityWithSchemaAndTableSpelExpression {
@Id private Long id;
public static String desiredTableName = "USE_THE_FORCE";
}
@Table(schema = "LITTLE_BOBBY_TABLES",
name="#{T(org.springframework.data.relational.core.mapping." +
"RelationalPersistentEntityImplUnitTests$LittleBobbyTables" +
").desiredTableName}")
private static class LittleBobbyTables {
@Id private Long id;
public static String desiredTableName = "Robert'); DROP TABLE students;--";
}
@Table("dummy_sub_entity")
static class DummySubEntity {
@Id @Column("renamedId") Long id;

Loading…
Cancel
Save