Skip to content

Commit

Permalink
Add SpEL support for @Table and @Column.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kurtn718 authored and mp911de committed May 31, 2023
1 parent 064b3d3 commit 68a13fe
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert
BasicJdbcPersistentProperty persistentProperty = new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder,
this.getNamingStrategy());
persistentProperty.setForceQuote(isForceQuote());
persistentProperty.setSpelExpressionProcessor(getSpelExpressionProcessor());
return persistentProperty;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
* @author Greg Turnquist
* @author Florian Lüdiger
* @author Bastian Wilhelm
* @author Kurt Niemi
*/
public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty<RelationalPersistentProperty>
implements RelationalPersistentProperty {
Expand All @@ -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}.
Expand Down Expand Up @@ -90,6 +92,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,

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))));
Expand All @@ -109,6 +112,13 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
.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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -77,12 +78,21 @@ public void setForceQuote(boolean forceQuote) {
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;
}
Expand All @@ -94,6 +104,7 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert
BasicRelationalPersistentProperty persistentProperty = new BasicRelationalPersistentProperty(property, owner,
simpleTypeHolder, this.namingStrategy);
persistentProperty.setForceQuote(isForceQuote());
persistentProperty.setSpelExpressionProcessor(getSpelExpressionProcessor());

return persistentProperty;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* @author Greg Turnquist
* @author Bastian Wilhelm
* @author Mikhail Polivakha
* @author Kurt Niemi
*/
class RelationalPersistentEntityImpl<T> extends BasicPersistentEntity<T, RelationalPersistentProperty>
implements RelationalPersistentEntity<T> {
Expand All @@ -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}.
Expand All @@ -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));

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
* @author Oliver Gierke
* @author Florian Lüdiger
* @author Bastian Wilhelm
* @author Kurt Niemi
*/
public class BasicRelationalPersistentPropertyUnitTests {

Expand Down Expand Up @@ -68,6 +69,19 @@ public void detectsAnnotatedColumnAndKeyName() {
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() {

Expand Down Expand Up @@ -149,6 +163,22 @@ private static class DummyEntity {
// 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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* @author Bastian Wilhelm
* @author Mark Paluch
* @author Mikhail Polivakha
* @author Kurt Niemi
*/
class RelationalPersistentEntityImplUnitTests {

Expand Down Expand Up @@ -96,6 +97,41 @@ void testRelationalPersistentEntitySchemaNameChoice() {
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() {

Expand All @@ -117,6 +153,24 @@ private static class EntityWithSchemaAndName {
@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;
Expand Down

0 comments on commit 68a13fe

Please sign in to comment.