0x01 Preface
This Tuesday, March 1, Springofficially publishedthe Spring Cloud Gateway CVE report

CVE-2022-22947, a Spring Cloud Gateway code-injection vulnerability, was rated Critical. Many analyses appeared on Wednesday and Thursday. Work and thesis writing delayed my review, but I reproduced and analyzed it over the weekend. It is an interesting issue.
0x02 Starting with SSRF
The exploit flow looked familiar. I checked Chen's earlier post and found exactly why:

Last December, Chen reported an Actuator Gateway SSRF originating from wya
The author explained that the Spring Cloud Gateway Actuator managementfunctionalitycan add, delete, and otherwise manage routes.

The author therefore used Actuator route creation and, according toOfficial example, as shown below:

adds a route:
POST /actuator/gateway/routes/new_route HTTP/1.1
Host: 127.0.0.1:9000
Connection: close
Content-Type: application/json
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/new_route/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"_genkey_0": "/new_route(?<path>.*)",
"_genkey_1": "/${path}"
}
}
],
"uri": "https://wya.pl",
"order": 0
}
While executing refresh operation, the author successfully issued an SSRF request (torequest to https://wya.pl/index.php):

Chen also provided a demonstration:https://github.com/API-Security/APISandbox/blob/main/OASystem/README.md
Without yet discussing why the payload has this shape, anyone familiar with CVE-2022-22947 will recognize it.
CVE-2022-22947 is an advanced form of this SSRF, and the SSRF trigger itself is straightforward.
First use/actuator/gateway/routes/{new route}to specify a URL and add a route for it
POST /actuator/gateway/routes/new_route HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Content-Type: application/json
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/new_route/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"_genkey_0": "/new_route(?<path>.*)",
"_genkey_1": "/${path}"
}
}
],
"uri": "https://www.cnpanda.net",
"order": 0
}
Then refresh to activate the route:
POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Content-Type: application/json
{
"predicate": "Paths: [/new_route], match trailing slash: true",
"route_id": "new_route",
"filters": [
"[[RewritePath /new_route(?<path>.*) = /${path}], order = 1]"
],
"uri": "https://www.cnpanda.net",
"order": 0
}
Finally visit/new_route/index.phptriggers SSRF.
Two questions remain:First, why is the payload written this way? Second, what is the complete request flow?
First answer the first question:Why is the payload written this way?
The official Spring Cloud Gateway example above is:
{
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
}
Comparing it with the SSRF payload shows that SSRF additionally defines filters.
Looking at the full payload, it is essentiallya dynamic route configuration process。
Spring Cloud Gateway supports static and dynamic routes. Static route changes require a restart. Because the gateway is often the entry point for all traffic, production systems avoid restarts and usually use dynamic routing for high availability.
Spring Cloud Gateway supports two dynamic-routing approaches. The common one implements custom dynamic routing in code, such asherecontains a dynamic-routing configuration process. The second is the SSRF method above, which lives only in JVM memory; a restart removes the added route. P also explained this inv2exexplanation given there

The payload has a fixed structure: define predicates to match user requests, then add built-in or custom filters for extra logic.
The payload uses RewritePath. Similar filters include SetPath and StripPrefix; seeGateway built-in filterthis diagram:

andGateway built-in GlobalFilterDiagram:

With the first question answered, consider the second:What is the complete request flow?
As in the example, when a browser sends127.0.0.1:8080address with root path/new_routerequest, Spring Cloud Gateway forwards it to/root path of
For example, send to127.0.0.1:8080address with a request for/new_route/index.phprequest, Spring Cloud Gateway forwards it to/index.phppath. The official documentation (Spring Cloud Gateway Workflow) summarizes the flow:

It looks simple but is more complex. I made a detailed diagram:

First send from the browserhttp://127.0.0.1:8080/new_route/index.php request, the browser sends it to Spring Cloud Gateway. Gateway Handler Mapping first finds the route matching/new_route/index.phprequest, then sends it to Gateway Web Handler. That module creates FilteringWebHandler with global filters including NettyWriteResponseFilter, ForwardPathFilter, RouteToRequestUrlFilter, LoadBalancerClientFilter, AdaptCachedBodyGlobalFilter, WebsocketRoutingFilter, NettyRoutingFilter, and ForwardRoutingFilter.
As shown, in NettyRoutingFilter shows the intermediate state of our request:

FilteringWebHandler runs the request filter chain. All pre-filters run first, then the proxy request reaches the proxied service. After its response returns, post-filters run, and finallyNettyWriteResponseFilter returns the response to us. For the response path, seeSpring Cloud Gateway Source Analysis—Filter 4.7: NettyRoutingFilter:

This completes a full SSRF request and response.
This SSRF is effectively a by-product of Spring Cloud Gateway functionality, comparable to an authenticated SQL injection in phpMyAdmin.
0x03 CVE-2022-22947 Analysis
If you read the previous section carefully, the vulnerability should now be clearer.
The trigger is the familiar SpEL expression.
Even without a full source analysis, the payload or official patch diff reveals this conclusion:During dynamic route creation, a filter parses an incoming value as SpEL, leading to remote code execution.
Is that really the case?
Following this idea, verify the chain by connecting source and sink in both directions.
First inspect the source—the route-creation payload:
{
"id": "hacktest",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
}
}],
"uri": "http://example.com"
}
The filter is AddResponseHeader. Since SpEL is suspected, search directly for its trigger.StandardEvaluationContext:

We can see that in ShortcutConfigurable interface'sgetValuemethod usesStandardEvaluationContext, and parses the incoming SpEL expression
Next search for ShortcutConfigurable implementations of the interface:

Many classes appear, but we need the AddResponseHeader filter factory: org.springframework.cloud.gateway.filter.factory#AddResponseHeaderGatewayFilterFactory, so the module name identifies its location:

Inspecting them one by one shows:
AddResponseHeaderGatewayFilterFactory inherits from AbstractNameValueGatewayFilterFactory
AbstractNameValueGatewayFilterFactory inherits from AbstractGatewayFilterFactory
AbstractGatewayFilterFactory implements GatewayFilterFactory interface
GatewayFilterFactory interface inherits from ShortcutConfigurable
Therefore, when from AddResponseHeaderGatewayFilterFactory When getValue() evaluates the input, calls climb the inheritance chain until the SpEL parser performs final evaluation, triggering expression injection.
Finally, enter AddResponseHeaderGatewayFilterFactory class for review:
public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
exchange.getResponse().getHeaders().add(config.getName(), value);
return chain.filter(exchange);
}
@Override
public String toString() {
return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this)
.append(config.getName(), config.getValue()).toString();
}
};
}
}
The apply method first receivesNameValueConfigconfig type. NameValueConfig contains two required non-null values:

NameValueConfig belongs to AbstractNameValueGatewayFilterFactory, the parent of AddResponseHeaderGatewayFilterFactory. The parent calls getValue(), and the value returned from config is the result of the SpEL expression:

0x04 Fix
SpEL injection is usually caused by using StandardEvaluationContext to parse expressions. Two methods parse expressions:
- SimpleEvaluationContext exposes only a deliberately restricted subset of SpEL for expression categories that do not require the full language.
- StandardEvaluationContext exposes the full SpEL language and configuration options, including a default root object and all evaluation strategies.
SimpleEvaluationContext supports only a SpEL subset and excludes Java type references, constructors, and bean references. StandardEvaluationContext supportsthe full SpEL syntax. Based on the feature description, setStandardEvaluationContextmethod uses SimpleEvaluationContext method instead.
The official fix uses BeanFactoryResolver to reference a bean, then passes it to the project's own parser methodGatewayEvaluationContextin:


The official documentation also providesRecommendation:

Disable the Gateway Actuator endpoint if unnecessary. If needed, protect it with Spring Security; see:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security
0x05 Closing Notes
The vulnerability is clear. Unfortunately, I did not dig deeper into Chen's earlier SSRF to discover it independently. Success really does favor attentive researchers.
Warning: in a real environment, a failed delete can make refresh fail and disrupt the site. Do not experiment carelessly or a restart may be required.
Does this vulnerability resemble an official in-memory web shell? (laughs)
Corrections are welcome if this article contains errors.
Note: another recommendation for Chen's knowledge community.

0x06 References
https://juejin.cn/post/6844903639840980999
https://blog.csdn.net/qq_38233650/article/details/98038225
https://github.com/vulhub/vulhub/blob/master/spring/CVE-2022-22947/README.zh-cn.md
https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e