This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 9f0c1ffa2e fix(jwt): prevent client-supplied headers from spoofing 
converted claims (#7967)
9f0c1ffa2e is described below

commit 9f0c1ffa2ee47d6f80949cfad379759349e1fd04
Author: Duansg <[email protected]>
AuthorDate: Thu Oct 22 20:16:46 2026 -0700

    fix(jwt): prevent client-supplied headers from spoofing converted claims 
(#7967)
---
 .../jwt/strategy/DefaultJwtConvertStrategy.java    | 41 ++++-----
 .../strategy/DefaultJwtConvertStrategyTest.java    | 98 ++++++++++++++++++++++
 2 files changed, 116 insertions(+), 23 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/main/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategy.java
 
b/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/main/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategy.java
index ecdf342a8e..6b5725092c 100644
--- 
a/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/main/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategy.java
+++ 
b/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/main/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategy.java
@@ -31,7 +31,7 @@ import org.springframework.web.server.ServerWebExchange;
 
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Objects;
 
 @Join
 public class DefaultJwtConvertStrategy implements JwtConvertStrategy {
@@ -85,10 +85,14 @@ public class DefaultJwtConvertStrategy implements 
JwtConvertStrategy {
                 continue;
             }
 
-            if (converter.getJwtVal().contains(".")) {
-                headers.add(converter.getHeaderVal(), parse(body, 
converter.getJwtVal().split("\\."), new AtomicInteger(0)));
+            final Object value = converter.getJwtVal().contains(".")
+                    ? parse(body, converter.getJwtVal().split("\\."))
+                    : body.get(converter.getJwtVal());
+            // overwrite rather than append, so a client-supplied header with 
the same name can not spoof the claim
+            if (Objects.isNull(value)) {
+                headers.remove(converter.getHeaderVal());
             } else {
-                headers.add(converter.getHeaderVal(), 
String.valueOf(body.get(converter.getJwtVal())));
+                headers.set(converter.getHeaderVal(), String.valueOf(value));
             }
 
         }
@@ -97,27 +101,18 @@ public class DefaultJwtConvertStrategy implements 
JwtConvertStrategy {
     /**
      * Parsing multi-level tokens.
      *
-     * @param body  token
-     * @param split jwt of key
-     * @param deep  level default 0
-     * @return token of val
+     * @param body token
+     * @param path jwt of key split by "."
+     * @return token of val, or null if any node on the path is absent or not 
a map
      */
-    private String parse(final Map<String, Object> body,
-                         final String[] split,
-                         final AtomicInteger deep) {
-        for (Map.Entry<String, Object> entry : body.entrySet()) {
-
-            if (deep.get() == split.length - 1) {
-                return String.valueOf(body.get(split[deep.get()]));
-            }
-
-            if (entry.getKey().equals(split[deep.get()])) {
-                if (entry.getValue() instanceof Map) {
-                    deep.incrementAndGet();
-                    return parse((Map<String, Object>) entry.getValue(), 
split, deep);
-                }
+    private Object parse(final Map<String, Object> body, final String[] path) {
+        Object current = body;
+        for (String key : path) {
+            if (!(current instanceof Map)) {
+                return null;
             }
+            current = ((Map<?, ?>) current).get(key);
         }
-        return String.valueOf(body.get(split[deep.get()]));
+        return current;
     }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/test/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategyTest.java
 
b/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/test/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategyTest.java
index e3da500be8..3ee8d0b4ec 100644
--- 
a/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/test/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategyTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-security/shenyu-plugin-jwt/src/test/java/org/apache/shenyu/plugin/jwt/strategy/DefaultJwtConvertStrategyTest.java
@@ -21,16 +21,20 @@ import com.google.common.collect.ImmutableMap;
 import org.apache.shenyu.plugin.jwt.rule.DefaultJwtRuleHandle;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
 import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
 import org.springframework.mock.web.server.MockServerWebExchange;
 import org.springframework.web.server.ServerWebExchange;
 
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class DefaultJwtConvertStrategyTest {
@@ -188,4 +192,98 @@ public class DefaultJwtConvertStrategyTest {
         assertEquals("true", headers.get("X-Can-Read").get(0));
     }
 
+    @Test
+    public void testConvertOverridesClientSuppliedHeader() {
+        ServerWebExchange spoofed = 
MockServerWebExchange.from(MockServerHttpRequest
+                .get("localhost")
+                .header("X-User-Id", "admin")
+                .header("X-Web", "evil")
+                .build());
+        String handleJson = 
"{\"converter\":[{\"jwtVal\":\"sub\",\"headerVal\":\"X-User-Id\"},"
+                + "{\"jwtVal\":\"multi.web\",\"headerVal\":\"X-Web\"}]}";
+        HttpHeaders headers = defaultJwtConvertStrategy
+                
.convert(defaultJwtConvertStrategy.parseHandleJson(handleJson), spoofed, 
jwtBody)
+                .getRequest().getHeaders();
+
+        assertEquals(List.of("12345"), headers.get("X-User-Id"));
+        assertEquals(List.of("shenyu"), headers.get("X-Web"));
+    }
+
+    @Test
+    public void testConvertRemovesHeaderWhenClaimMissing() {
+        ServerWebExchange spoofed = 
MockServerWebExchange.from(MockServerHttpRequest
+                .get("localhost")
+                .header("X-Tenant", "evil")
+                .header("X-Nested", "evil")
+                .build());
+        String handleJson = 
"{\"converter\":[{\"jwtVal\":\"tenant\",\"headerVal\":\"X-Tenant\"},"
+                + "{\"jwtVal\":\"multi.absent\",\"headerVal\":\"X-Nested\"}]}";
+        HttpHeaders headers = defaultJwtConvertStrategy
+                
.convert(defaultJwtConvertStrategy.parseHandleJson(handleJson), spoofed, 
jwtBody)
+                .getRequest().getHeaders();
+
+        assertFalse(headers.containsKey("X-Tenant"));
+        assertFalse(headers.containsKey("X-Nested"));
+    }
+
+    @Test
+    public void testConvertRemovesHeaderWhenIntermediateNodeIsNotMap() {
+        ServerWebExchange spoofed = 
MockServerWebExchange.from(MockServerHttpRequest
+                .get("localhost")
+                .header("X-User-Id", "evil")
+                .header("X-Deep", "evil")
+                .build());
+        Map<String, Object> body = ImmutableMap.of("user", "admin",
+                "multi", ImmutableMap.of("web", "shenyu"));
+        String handleJson = 
"{\"converter\":[{\"jwtVal\":\"user.id\",\"headerVal\":\"X-User-Id\"},"
+                + "{\"jwtVal\":\"multi.web.name\",\"headerVal\":\"X-Deep\"}]}";
+        HttpHeaders headers = defaultJwtConvertStrategy
+                
.convert(defaultJwtConvertStrategy.parseHandleJson(handleJson), spoofed, body)
+                .getRequest().getHeaders();
+
+        assertFalse(headers.containsKey("X-User-Id"));
+        assertFalse(headers.containsKey("X-Deep"));
+    }
+
+    @Test
+    public void testConvertOverridesMixedCaseAndMultiValueHeaders() {
+        ServerWebExchange spoofed = 
MockServerWebExchange.from(MockServerHttpRequest
+                .get("localhost")
+                .header("x-user-id", "admin")
+                .header("X-USER-ID", "root")
+                .header("X-Web", "evil1", "evil2")
+                .build());
+        String handleJson = 
"{\"converter\":[{\"jwtVal\":\"sub\",\"headerVal\":\"X-User-Id\"},"
+                + "{\"jwtVal\":\"multi.web\",\"headerVal\":\"x-web\"}]}";
+        HttpHeaders headers = defaultJwtConvertStrategy
+                
.convert(defaultJwtConvertStrategy.parseHandleJson(handleJson), spoofed, 
jwtBody)
+                .getRequest().getHeaders();
+
+        assertEquals(List.of("12345"), headers.get("x-user-id"));
+        assertEquals(List.of("12345"), headers.get("X-User-Id"));
+        assertEquals(List.of("shenyu"), headers.get("X-Web"));
+    }
+
+    @Test
+    public void testConvertRemovesHeaderWhenClaimIsExplicitNull() {
+        Map<String, Object> nested = new HashMap<>();
+        nested.put("tenant", null);
+        Map<String, Object> body = new HashMap<>();
+        body.put("tenant", null);
+        body.put("multi", nested);
+        ServerWebExchange spoofed = 
MockServerWebExchange.from(MockServerHttpRequest
+                .get("localhost")
+                .header("X-Tenant", "evil")
+                .header("X-Nested", "evil1", "evil2")
+                .build());
+        String handleJson = 
"{\"converter\":[{\"jwtVal\":\"tenant\",\"headerVal\":\"x-tenant\"},"
+                + "{\"jwtVal\":\"multi.tenant\",\"headerVal\":\"X-Nested\"}]}";
+        HttpHeaders headers = defaultJwtConvertStrategy
+                
.convert(defaultJwtConvertStrategy.parseHandleJson(handleJson), spoofed, body)
+                .getRequest().getHeaders();
+
+        assertFalse(headers.containsKey("X-Tenant"));
+        assertFalse(headers.containsKey("X-Nested"));
+    }
+
 }

Reply via email to