Summary
ConfigurationUtils.normalize(...) parses each value of a gray-decision's configuration Map as a
Spring Expression Language (SpEL) template and evaluates it in an unrestricted
StandardEvaluationContext with a BeanFactoryResolver attached. By creating an arbitrary gray
decision whose infos map contains a #{...} SpEL payload as a value, a subsequent inter-service
call triggers the policy load path, which in turn calls ConfigurationUtils.normalize on the
attacker-controlled values — leading to Remote Code Execution on the consumer service. This was
verified end-to-end against a sample setup (service-a + service-b).
Affected version / component
spring-cloud-gray D.0.0.2
- Sink:
spring-cloud-gray-utils — cn.springcloud.gray.utils.ConfigurationUtils
- Trigger path:
DefaultGrayDecisionFactoryKeeper.getGrayDecision(...) → ConfigurationUtils.normalize(infos, parser, beanFactory) (invoked on every load of decision policies)
Root cause (code excerpt)
// spring-cloud-gray-utils/.../ConfigurationUtils.java
public static Map<String, Object> normalize(Map<String, String> args,
SpelExpressionParser parser,
BeanFactory beanFactory) { // L28
Map<String, Object> map = new HashMap<>();
for (Map.Entry<String, String> entry : args.entrySet()) {
Object value = getValue(parser, beanFactory, entry.getValue()); // L34 every value normalized
map.put(entry.getKey(), value);
}
return map;
}
private static Object getValue(SpelExpressionParser parser, BeanFactory beanFactory,
String entryValue) { // L74
Object value;
String rawValue = entryValue;
if (rawValue != null) rawValue = rawValue.trim();
if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) { // L81 treat as SpEL template
StandardEvaluationContext context = new StandardEvaluationContext(); // L83 no sandbox
if (Objects.nonNull(beanFactory)) {
context.setBeanResolver(new BeanFactoryResolver(beanFactory)); // L85 ★ @bean lookups enabled
}
Expression expression = parser.parseExpression(entryValue, // L87
new TemplateParserContext()); // L88
value = expression.getValue(context); // L90 RCE
} else {
value = entryValue;
}
return value;
}
Untrusted decision-config Map<String,String> values are passed straight to
parseExpression(..., TemplateParserContext).getValue(StandardEvaluationContext) — full SpEL
capability: type references (T(...)), constructors, arbitrary method calls. The added
BeanFactoryResolver even lets the expression reach into the Spring BeanFactory via @beanName
syntax, which is an additional class of compromise (e.g. tampering with service beans / pivoting
within the JVM).
Verified exploit chain (per analysis)
- Plant — create an arbitrary gray decision whose
infos map contains, for any key, a value
that begins with #{ and ends with } (a SpEL template) and embeds the payload. The decision is
saved into PolicyDecisionManager. No evaluation yet — the value is just stored.
- Wire — register a producer
service-a and a consumer service-b, attaching the planted
policy.
- Trigger — send any request to the consumer that causes it to call the producer, e.g.
curl 127.0.0.1:20102/api/test/restTemplateGet
The consumer evaluates whether to apply gray routing for service-a. To do that it loads all
decision policies from PolicyDecisionManager, which calls
DefaultGrayDecisionFactoryKeeper.getGrayDecision(...). That call hits
ConfigurationUtils.normalize(infos, parser, beanFactory) — every #{...} value is parsed and
evaluated, and the planted payload executes on the consumer service host (RCE).
The downstream "should we route this request to gray?" check is moot — the code has already run
during the policy-load step.
Proof of concept (non-destructive)
Use a benign SpEL payload (e.g. a value that invokes T(java.lang.Runtime).getRuntime().exec('calc')
to launch calc as a harmless placeholder) as one of the infos values when creating the gray
decision. A real attacker would substitute an arbitrary command.
Impact
Remote Code Execution on any consumer service that loads gray-decision policies — i.e. any service
that participates in gray routing. The exact privilege required is "ability to create/modify a gray
decision" (i.e. influence the persisted infos map). Because BeanFactoryResolver is registered on
the evaluation context, the attacker also gets access to Spring beans inside the process.
- Tentative severity: Critical (RCE) —
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. The PR
estimate depends on who is authorized to write gray decisions in a given deployment.
Suggested remediation
- Do not evaluate decision-config strings as SpEL by default. The
#{...} template feature is
surprising for what looks like configuration data.
- If template substitution must be kept, use a restricted
SimpleEvaluationContext (no T(...), no
constructors, no arbitrary method calls; only the property accessors actually needed). Do not
attach a BeanFactoryResolver.
- Alternatively, validate values on write (reject
#{...} constructs unless an explicit trusted
template flag is set).
Notes for triage
- The chain was verified end-to-end against a sample setup (
service-a + service-b), curl request
as above. Happy to share additional reproduction details privately if useful.
- I have not disclosed this publicly elsewhere.
Thanks for triaging.
Summary
ConfigurationUtils.normalize(...)parses each value of a gray-decision's configurationMapas aSpring Expression Language (SpEL) template and evaluates it in an unrestricted
StandardEvaluationContextwith aBeanFactoryResolverattached. By creating an arbitrary graydecision whose
infosmap contains a#{...}SpEL payload as a value, a subsequent inter-servicecall triggers the policy load path, which in turn calls
ConfigurationUtils.normalizeon theattacker-controlled values — leading to Remote Code Execution on the consumer service. This was
verified end-to-end against a sample setup (
service-a+service-b).Affected version / component
spring-cloud-grayD.0.0.2spring-cloud-gray-utils—cn.springcloud.gray.utils.ConfigurationUtilsDefaultGrayDecisionFactoryKeeper.getGrayDecision(...)→ConfigurationUtils.normalize(infos, parser, beanFactory)(invoked on every load of decision policies)Root cause (code excerpt)
Untrusted decision-config
Map<String,String>values are passed straight toparseExpression(..., TemplateParserContext).getValue(StandardEvaluationContext)— full SpELcapability: type references (
T(...)), constructors, arbitrary method calls. The addedBeanFactoryResolvereven lets the expression reach into the SpringBeanFactoryvia@beanNamesyntax, which is an additional class of compromise (e.g. tampering with service beans / pivoting
within the JVM).
Verified exploit chain (per analysis)
infosmap contains, for any key, a valuethat begins with
#{and ends with}(a SpEL template) and embeds the payload. The decision issaved into
PolicyDecisionManager. No evaluation yet — the value is just stored.service-aand a consumerservice-b, attaching the plantedpolicy.
service-a. To do that it loads alldecision policies from
PolicyDecisionManager, which callsDefaultGrayDecisionFactoryKeeper.getGrayDecision(...). That call hitsConfigurationUtils.normalize(infos, parser, beanFactory)— every#{...}value is parsed andevaluated, and the planted payload executes on the consumer service host (RCE).
The downstream "should we route this request to gray?" check is moot — the code has already run
during the policy-load step.
Proof of concept (non-destructive)
Use a benign SpEL payload (e.g. a value that invokes
T(java.lang.Runtime).getRuntime().exec('calc')to launch
calcas a harmless placeholder) as one of theinfosvalues when creating the graydecision. A real attacker would substitute an arbitrary command.
Impact
Remote Code Execution on any consumer service that loads gray-decision policies — i.e. any service
that participates in gray routing. The exact privilege required is "ability to create/modify a gray
decision" (i.e. influence the persisted
infosmap). BecauseBeanFactoryResolveris registered onthe evaluation context, the attacker also gets access to Spring beans inside the process.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. The PRestimate depends on who is authorized to write gray decisions in a given deployment.
Suggested remediation
#{...}template feature issurprising for what looks like configuration data.
SimpleEvaluationContext(noT(...), noconstructors, no arbitrary method calls; only the property accessors actually needed). Do not
attach a
BeanFactoryResolver.#{...}constructs unless an explicit trustedtemplate flag is set).
Notes for triage
service-a+service-b), curl requestas above. Happy to share additional reproduction details privately if useful.
Thanks for triaging.