diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java index 43456533..a620e48b 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java @@ -25,10 +25,23 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.env.Environment; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.TypedValue; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.ReflectivePropertyAccessor; +import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -54,8 +67,7 @@ public interface ShortcutConfigurable { } if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) { // assume it's spel - StandardEvaluationContext context = new StandardEvaluationContext(); - context.setBeanResolver(new BeanFactoryResolver(beanFactory)); + GatewayEvaluationContext context = new GatewayEvaluationContext(beanFactory); Expression expression = parser.parseExpression(entryValue, new TemplateParserContext()); value = expression.getValue(context); } @@ -148,4 +160,92 @@ public interface ShortcutConfigurable { } + class GatewayEvaluationContext implements EvaluationContext { + + private final BeanFactoryResolver beanFactoryResolver; + + private final SimpleEvaluationContext delegate; + + public GatewayEvaluationContext(BeanFactory beanFactory) { + this.beanFactoryResolver = new BeanFactoryResolver(beanFactory); + Environment env = beanFactory.getBean(Environment.class); + boolean restrictive = env.getProperty("spring.cloud.gateway.restrictive-property-accessor.enabled", + Boolean.class, true); + if (restrictive) { + delegate = SimpleEvaluationContext.forPropertyAccessors(new RestrictivePropertyAccessor()) + .withMethodResolvers((context, targetObject, name, argumentTypes) -> null).build(); + } + else { + delegate = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + } + } + + @Override + public TypedValue getRootObject() { + return delegate.getRootObject(); + } + + @Override + public List getPropertyAccessors() { + return delegate.getPropertyAccessors(); + } + + @Override + public List getConstructorResolvers() { + return delegate.getConstructorResolvers(); + } + + @Override + public List getMethodResolvers() { + return delegate.getMethodResolvers(); + } + + @Override + @Nullable + public BeanResolver getBeanResolver() { + return this.beanFactoryResolver; + } + + @Override + public TypeLocator getTypeLocator() { + return delegate.getTypeLocator(); + } + + @Override + public TypeConverter getTypeConverter() { + return delegate.getTypeConverter(); + } + + @Override + public TypeComparator getTypeComparator() { + return delegate.getTypeComparator(); + } + + @Override + public OperatorOverloader getOperatorOverloader() { + return delegate.getOperatorOverloader(); + } + + @Override + public void setVariable(String name, Object value) { + delegate.setVariable(name, value); + } + + @Override + @Nullable + public Object lookupVariable(String name) { + return delegate.lookupVariable(name); + } + + } + + class RestrictivePropertyAccessor extends ReflectivePropertyAccessor { + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) { + return false; + } + + } + } diff --git a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 0a78082d..807ed20c 100644 --- a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -371,6 +371,12 @@ "type": "java.lang.Integer", "description": "The order of RoutePredicateHandlerMapping.", "defaultValue": "1" + }, + { + "name": "spring.cloud.gateway.restrictive-property-accessor.enabled", + "type": "java.lang.Boolean", + "description": "Restricts method and property access in SpEL.", + "defaultValue": "true" } ] } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java index 54ac5dbc..42f7ef19 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java @@ -28,12 +28,16 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @RunWith(SpringRunner.class) @SpringBootTest @@ -42,8 +46,64 @@ public class ShortcutConfigurableTests { @Autowired BeanFactory beanFactory; + @Autowired + ConfigurableEnvironment env; + private SpelExpressionParser parser; + @Test + public void testNormalizeDefaultTypeWithSpelAndInvalidInputFails() { + parser = new SpelExpressionParser(); + ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() { + @Override + public List shortcutFieldOrder() { + return Arrays.asList("bean", "arg1"); + } + }; + Map args = new HashMap<>(); + args.put("bean", "#{T(java.lang.Runtime).getRuntime().exec(\"touch /tmp/x\")}"); + args.put("arg1", "val1"); + assertThatThrownBy(() -> { + ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory); + }).isInstanceOf(SpelEvaluationException.class); + } + + @Test + public void testNormalizeDefaultTypeWithSpelAndInvalidPropertyReferenceFails() { + TestPropertyValues.of("spring.cloud.gateway.restrictive-property-accessor.enabled=true").applyTo(env); + parser = new SpelExpressionParser(); + ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() { + @Override + public List shortcutFieldOrder() { + return Arrays.asList("bean", "arg1"); + } + }; + Map args = new HashMap<>(); + args.put("barproperty", "#{@bar.getInt}"); + args.put("arg1", "val1"); + assertThatThrownBy(() -> { + ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory); + }).isInstanceOf(SpelEvaluationException.class); + } + + @Test + public void testNormalizeDefaultTypeWithSpelAndInvalidMethodReferenceFails() { + TestPropertyValues.of("ispring.cloud.gateway.restrictve-property-accessor.enabled=true").applyTo(env); + parser = new SpelExpressionParser(); + ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() { + @Override + public List shortcutFieldOrder() { + return Arrays.asList("bean", "arg1"); + } + }; + Map args = new HashMap<>(); + args.put("barmethod", "#{@bar.myMethod}"); + args.put("arg1", "val1"); + assertThatThrownBy(() -> { + ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory); + }).isInstanceOf(SpelEvaluationException.class); + } + @Test public void testNormalizeDefaultTypeWithSpel() { parser = new SpelExpressionParser(); @@ -60,6 +120,40 @@ public class ShortcutConfigurableTests { assertThat(map).isNotNull().containsEntry("bean", 42).containsEntry("arg1", "val1"); } + @Test + public void testNormalizeDefaultTypeWithSpelAndPropertyReferenceEnabled() { + TestPropertyValues.of("spring.cloud.gateway.restrictive-property-accessor.enabled=false").applyTo(env); + parser = new SpelExpressionParser(); + ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() { + @Override + public List shortcutFieldOrder() { + return Arrays.asList("bean", "arg1"); + } + }; + Map args = new HashMap<>(); + args.put("barproperty", "#{@bar.getInt}"); + args.put("arg1", "val1"); + Map map = ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory); + assertThat(map).isNotNull().containsEntry("barproperty", 42).containsEntry("arg1", "val1"); + } + + @Test + public void testNormalizeDefaultTypeWithSpelAndMethodReferenceEnabled() { + TestPropertyValues.of("spring.cloud.gateway.restrictive-property-accessor.enabled=false").applyTo(env); + parser = new SpelExpressionParser(); + ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() { + @Override + public List shortcutFieldOrder() { + return Arrays.asList("bean", "arg1"); + } + }; + Map args = new HashMap<>(); + args.put("barmethod", "#{@bar.myMethod}"); + args.put("arg1", "val1"); + Map map = ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory); + assertThat(map).isNotNull().containsEntry("barmethod", 42).containsEntry("arg1", "val1"); + } + @Test @SuppressWarnings("unchecked") public void testNormalizeGatherListTypeWithSpel() { @@ -136,6 +230,23 @@ public class ShortcutConfigurableTests { return 42; } + @Bean + public Bar bar() { + return new Bar(); + } + + } + + protected static class Bar { + + public int getInt() { + return 42; + } + + public int myMethod() { + return 42; + } + } }