From 68a13fe12b03654167a2429d7414d470572e0f54 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 24 Mar 2023 15:16:55 -0400 Subject: [PATCH] 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 --- .../jdbc/core/mapping/JdbcMappingContext.java | 1 + .../BasicRelationalPersistentProperty.java | 10 +++ .../mapping/RelationalMappingContext.java | 11 +++ .../RelationalPersistentEntityImpl.java | 11 +++ .../core/mapping/SpelExpressionProcessor.java | 87 +++++++++++++++++++ .../SpelExpressionResultSanitizer.java | 10 +++ ...RelationalPersistentPropertyUnitTests.java | 30 +++++++ ...lationalPersistentEntityImplUnitTests.java | 54 ++++++++++++ 8 files changed, 214 insertions(+) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionProcessor.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionResultSanitizer.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java index ef574390bd..0a13fd61fe 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java @@ -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; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index f372e8bb01..a5f30b01d1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -37,6 +37,7 @@ * @author Greg Turnquist * @author Florian Lüdiger * @author Bastian Wilhelm + * @author Kurt Niemi */ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty implements RelationalPersistentProperty { @@ -48,6 +49,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent private final Lazy embeddedPrefix; private final NamingStrategy namingStrategy; private boolean forceQuote = true; + private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor(); /** * Creates a new {@link BasicRelationalPersistentProperty}. @@ -90,6 +92,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity 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 BasicRelationalPersistentProperty(Property property, PersistentEntity 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); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index c6712a4c9a..5e3be949a8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -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 void setForceQuote(boolean forceQuote) { this.forceQuote = forceQuote; } + public SpelExpressionProcessor getSpelExpressionProcessor() { + return spelExpressionProcessor; + } + + public void setSpelExpressionProcessor(SpelExpressionProcessor spelExpressionProcessor) { + this.spelExpressionProcessor = spelExpressionProcessor; + } + @Override protected RelationalPersistentEntity createPersistentEntity(TypeInformation typeInformation) { RelationalPersistentEntityImpl entity = new RelationalPersistentEntityImpl<>(typeInformation, this.namingStrategy); entity.setForceQuote(isForceQuote()); + entity.setSpelExpressionProcessor(getSpelExpressionProcessor()); return entity; } @@ -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; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java index 6bab03ff8c..29b62e0635 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java @@ -31,6 +31,7 @@ * @author Greg Turnquist * @author Bastian Wilhelm * @author Mikhail Polivakha + * @author Kurt Niemi */ class RelationalPersistentEntityImpl extends BasicPersistentEntity implements RelationalPersistentEntity { @@ -39,6 +40,7 @@ class RelationalPersistentEntityImpl extends BasicPersistentEntity> tableName; private final Lazy> 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 extends BasicPersistentEntity Optional.ofNullable(findAnnotation(Table.class)) // .map(Table::value) // + .map(spelExpressionProcessor::applySpelExpression) // .filter(StringUtils::hasText) // .map(this::createSqlIdentifier)); @@ -62,6 +65,14 @@ class RelationalPersistentEntityImpl extends BasicPersistentEntity 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 @@ 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;