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

hefengen 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 31f0a4d8a7 Stop exposing login AES secrets to anonymous callers (#6323)
31f0a4d8a7 is described below

commit 31f0a4d8a7e2ae99beafcb2523e67419ac11b81a
Author: aias00 <[email protected]>
AuthorDate: Wed Apr 15 19:39:54 2026 +0800

    Stop exposing login AES secrets to anonymous callers (#6323)
    
    * goalx: snapshot before shenyu-analysis
    
    * Stop exposing login AES secrets to anonymous callers
    
    The login page was fetching /platform/secretInfo anonymously and receiving
    the long-lived AES key material used for password encryption. This
    change sanitizes the endpoint output so no real key/IV is returned and
    adds a server-side fallback to accept plain-text passwords when the
    client no longer encrypts before login.
    
    Constraint: Preserve compatibility with the existing bundled login page 
without rebuilding the frontend assets
    Rejected: Remove the endpoint or require auth only | would leave the 
current login bundle unable to encrypt and cause login failures without a 
backend fallback
    Confidence: medium
    Scope-risk: moderate
    Reversibility: clean
    Directive: Treat any future login bootstrap material as public-only data; 
never return reusable symmetric secrets to the client
    Tested: mvn -pl shenyu-admin 
-Dtest=SecretServiceTest,DashboardUserServiceTest,PlatformControllerTest test
    Not-tested: End-to-end browser login flow against a running admin instance 
and non-TLS deployment behavior
    
    * Refine login secret exposure fix after review
    
    This follow-up removes an unused constructor dependency, fixes the
    secret payload encoding charset explicitly, and narrows AES decrypt
    attempts to inputs that look like ciphertext so normal plain-text login
    fallback no longer produces avoidable decrypt failures.
    
    Constraint: Maintain compatibility with the current login bundle while 
reducing operational noise from the fallback path
    Confidence: high
    Scope-risk: moderate
    Reversibility: clean
    Directive: Avoid using exception-driven control flow for common login paths 
when ciphertext can be identified up front
    Tested: mvn -pl shenyu-admin 
-Dtest=SecretServiceTest,DashboardUserServiceTest,PlatformControllerTest test
    Not-tested: End-to-end browser login against a running admin instance
    
    * Reduce login fallback log noise after review
    
    This follow-up removes the exception-driven decrypt path from normal
    plain-text login fallback and switches secret bootstrap encoding to an
    explicit UTF-8 byte conversion while keeping the sanitized response.
    
    Constraint: Preserve compatibility with the current bundled login page 
while avoiding production error-log spam on valid fallback flows
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Do not route expected login fallback behavior through utilities 
that log hard failures by default
    Tested: mvn -pl shenyu-admin 
-Dtest=SecretServiceTest,DashboardUserServiceTest,PlatformControllerTest test
    Not-tested: End-to-end browser login against a running admin instance
    
    * Replace login secret magic values with named constants
    
    This follow-up names the AES block size and the sanitized secret
    placeholder in production code, and replaces repeated literal key/iv
    and credential strings in the related tests with explicit constants.
    
    Constraint: Keep the cleanup local to the active login-secret fix to avoid 
unrelated churn across other PR branches
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Prefer named constants for repeated protocol/security values 
even in tests when they carry semantic meaning
    Tested: mvn -pl shenyu-admin 
-Dtest=SecretServiceTest,DashboardUserServiceTest,PlatformControllerTest test
    Not-tested: Full shenyu-admin test suite
    
    * Refine login secret fixes after latest review
    
    This follow-up removes the fully qualified SecretProperties usage in
    SecretServiceImpl and factors the login password normalization into a
    named helper so the plain-text fallback path reads clearly in the main
    login flow.
    
    Constraint: Keep the update local to the existing login-secret fix without 
changing the external API shape
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Keep security-sensitive fallback logic explicit and readable; 
avoid hiding behavior in dense Optional chains
    Tested: mvn -pl shenyu-admin 
-Dtest=SecretServiceTest,DashboardUserServiceTest,PlatformControllerTest test
    Not-tested: End-to-end browser login against a running admin instance
---
 .../service/impl/DashboardUserServiceImpl.java     | 55 +++++++++++++++++---
 .../admin/service/impl/SecretServiceImpl.java      | 12 ++---
 .../admin/controller/PlatformControllerTest.java   | 16 ++++--
 .../admin/service/DashboardUserServiceTest.java    | 16 ++++--
 .../shenyu/admin/service/SecretServiceTest.java    | 58 ++++++++++++++++++++++
 5 files changed, 136 insertions(+), 21 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
index 18b41cd7c0..87ea9546c2 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
@@ -52,9 +52,9 @@ import org.apache.shenyu.admin.utils.SessionUtil;
 import org.apache.shenyu.admin.utils.WebI18nAssert;
 import org.apache.shenyu.common.constant.AdminConstants;
 import org.apache.shenyu.common.constant.Constants;
-import org.apache.shenyu.common.utils.AesUtils;
 import org.apache.shenyu.common.utils.DigestUtils;
 import org.apache.shenyu.common.utils.ListUtil;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.ldap.NameNotFoundException;
@@ -63,6 +63,13 @@ import org.springframework.ldap.support.LdapEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.Security;
+import java.util.Base64;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -77,6 +84,8 @@ public class DashboardUserServiceImpl implements 
DashboardUserService {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(DashboardUserServiceImpl.class);
 
+    private static final int AES_BLOCK_SIZE = 16;
+
     private final DashboardUserMapper dashboardUserMapper;
 
     private final UserRoleMapper userRoleMapper;
@@ -279,12 +288,7 @@ public class DashboardUserServiceImpl implements 
DashboardUserService {
     @Override
     public LoginDashboardUserVO login(final String userName, final String 
password, final String clientId) {
         DashboardUserVO dashboardUserVO = null;
-        final String cbcDecryptPassword;
-        if (StringUtils.isNotBlank(secretProperties.getKey()) && 
StringUtils.isNotBlank(secretProperties.getIv())) {
-            cbcDecryptPassword = 
AesUtils.cbcDecrypt(secretProperties.getKey(), secretProperties.getIv(), 
password);
-        } else {
-            cbcDecryptPassword = password;
-        }
+        final String cbcDecryptPassword = resolveLoginPassword(password);
 
         if (Objects.nonNull(ldapTemplate)) {
             dashboardUserVO = loginByLdap(userName, cbcDecryptPassword);
@@ -313,6 +317,43 @@ public class DashboardUserServiceImpl implements 
DashboardUserService {
                 .orElse(null);
     }
 
+    private String resolveLoginPassword(final String password) {
+        if (StringUtils.isNotBlank(secretProperties.getKey()) && 
StringUtils.isNotBlank(secretProperties.getIv())
+                && isPotentialEncryptedPassword(password)) {
+            return tryDecryptPassword(password).orElse(password);
+        }
+        return password;
+    }
+
+    private boolean isPotentialEncryptedPassword(final String password) {
+        if (StringUtils.isBlank(password)) {
+            return false;
+        }
+        try {
+            byte[] decoded = Base64.getDecoder().decode(password);
+            return decoded.length > 0 && decoded.length % AES_BLOCK_SIZE == 0;
+        } catch (IllegalArgumentException ignored) {
+            return false;
+        }
+    }
+
+    private Optional<String> tryDecryptPassword(final String password) {
+        Security.addProvider(new BouncyCastleProvider());
+        byte[] secretKeyBytes = 
secretProperties.getKey().getBytes(StandardCharsets.UTF_8);
+        byte[] ivBytes = 
secretProperties.getIv().getBytes(StandardCharsets.UTF_8);
+        try {
+            SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");
+            Cipher cipher = Cipher.getInstance("AES/CBC/Pkcs7Padding");
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
+            byte[] decryptedBytes = 
cipher.doFinal(Base64.getDecoder().decode(password));
+            return Optional.of(new String(decryptedBytes, 
StandardCharsets.UTF_8));
+        } catch (Exception e) {
+            LOG.debug("AES login password decrypt failed, falling back to 
plain text password", e);
+            return Optional.empty();
+        }
+    }
+
     /**
      * modify password.
      *
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SecretServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SecretServiceImpl.java
index 9e1a2596c4..aad23dc6ec 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SecretServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SecretServiceImpl.java
@@ -17,6 +17,7 @@
 
 package org.apache.shenyu.admin.service.impl;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import org.apache.shenyu.admin.config.properties.SecretProperties;
 import org.apache.shenyu.admin.service.SecretService;
@@ -29,14 +30,13 @@ import org.springframework.stereotype.Service;
 @Service
 public class SecretServiceImpl implements SecretService {
 
-    private final SecretProperties secretProperties;
-
-    public SecretServiceImpl(final SecretProperties secretProperties) {
-        this.secretProperties = secretProperties;
-    }
+    private static final String SANITIZED_SECRET_VALUE = "";
 
     @Override
     public String info() {
-        return 
Base64.getEncoder().encodeToString(JsonUtils.toJson(secretProperties).getBytes());
+        SecretProperties sanitized = new SecretProperties();
+        sanitized.setKey(SANITIZED_SECRET_VALUE);
+        sanitized.setIv(SANITIZED_SECRET_VALUE);
+        return 
Base64.getEncoder().encodeToString(JsonUtils.toJson(sanitized).getBytes(StandardCharsets.UTF_8));
     }
 }
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PlatformControllerTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PlatformControllerTest.java
index e78bbe9a16..25dfbba730 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PlatformControllerTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/PlatformControllerTest.java
@@ -52,6 +52,14 @@ import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @ExtendWith(MockitoExtension.class)
 public final class PlatformControllerTest {
 
+    private static final String TEST_LOGIN_USER_NAME = "admin";
+
+    private static final String TEST_LOGIN_PASSWORD = "123456";
+
+    private static final String TEST_USER_ID = "1";
+
+    private static final String TEST_STORED_PASSWORD = "2095132720951327";
+
     private MockMvc mockMvc;
 
     @InjectMocks
@@ -69,8 +77,8 @@ public final class PlatformControllerTest {
     /**
      * dashboardUser mock data.
      */
-    private final DashboardUserVO dashboardUserVO = new DashboardUserVO("1", 
"admin", "2095132720951327",
-            1, true, "1", DateUtils.localDateTimeToString(LocalDateTime.now()),
+    private final DashboardUserVO dashboardUserVO = new 
DashboardUserVO(TEST_USER_ID, TEST_LOGIN_USER_NAME, TEST_STORED_PASSWORD,
+            1, true, TEST_USER_ID, 
DateUtils.localDateTimeToString(LocalDateTime.now()),
             DateUtils.localDateTimeToString(LocalDateTime.now()));
 
     /**
@@ -86,10 +94,10 @@ public final class PlatformControllerTest {
      */
     @Test
     public void testLoginDashboardUser() throws Exception {
-        final String loginUri = 
"/platform/login?userName=admin&password=123456";
+        final String loginUri = "/platform/login?userName=" + 
TEST_LOGIN_USER_NAME + "&password=" + TEST_LOGIN_PASSWORD;
 
         LoginDashboardUserVO loginDashboardUserVO = 
LoginDashboardUserVO.buildLoginDashboardUserVO(dashboardUserVO);
-        given(this.dashboardUserService.login(eq("admin"), eq("123456"), 
isNull())).willReturn(loginDashboardUserVO);
+        given(this.dashboardUserService.login(eq(TEST_LOGIN_USER_NAME), 
eq(TEST_LOGIN_PASSWORD), isNull())).willReturn(loginDashboardUserVO);
         this.mockMvc.perform(MockMvcRequestBuilders.request(HttpMethod.GET, 
loginUri))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.code", is(CommonErrorCode.SUCCESSFUL)))
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
index d1ca7ed01b..791496e8c2 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
@@ -77,6 +77,10 @@ public final class DashboardUserServiceTest {
 
     public static final String TEST_PASSWORD = "password";
 
+    private static final String TEST_AES_KEY = "2095132720951327";
+
+    private static final String TEST_AES_IV = "6075877187097700";
+
     @InjectMocks
     private DashboardUserServiceImpl dashboardUserService;
 
@@ -223,15 +227,19 @@ public final class DashboardUserServiceTest {
 
         // test loginByDatabase AES password
         SecretProperties secretPropertiesTmp = new SecretProperties();
-        secretPropertiesTmp.setKey("2095132720951327");
-        secretPropertiesTmp.setIv("6075877187097700");
+        secretPropertiesTmp.setKey(TEST_AES_KEY);
+        secretPropertiesTmp.setIv(TEST_AES_IV);
         ReflectionTestUtils.setField(dashboardUserService, "secretProperties", 
secretPropertiesTmp);
         ReflectionTestUtils.setField(dashboardUserService, "ldapTemplate", 
null);
-        assertLoginSuccessful(dashboardUserDO, 
dashboardUserService.login(TEST_USER_NAME, 
AesUtils.cbcEncrypt("2095132720951327", "6075877187097700", TEST_PASSWORD), 
null));
+        assertLoginSuccessful(dashboardUserDO, 
dashboardUserService.login(TEST_USER_NAME, AesUtils.cbcEncrypt(TEST_AES_KEY, 
TEST_AES_IV, TEST_PASSWORD), null));
         verify(dashboardUserMapper, times(3)).findByQuery(eq(TEST_USER_NAME), 
anyString());
-        assertLoginSuccessful(dashboardUserDO, 
dashboardUserService.login(TEST_USER_NAME, 
AesUtils.cbcEncrypt("2095132720951327", "6075877187097700", TEST_PASSWORD), 
null));
+        assertLoginSuccessful(dashboardUserDO, 
dashboardUserService.login(TEST_USER_NAME, AesUtils.cbcEncrypt(TEST_AES_KEY, 
TEST_AES_IV, TEST_PASSWORD), null));
         verify(dashboardUserMapper, times(4)).findByQuery(eq(TEST_USER_NAME), 
anyString());
 
+        // test loginByDatabase plain password fallback when secret endpoint 
does not provide key material
+        assertLoginSuccessful(dashboardUserDO, 
dashboardUserService.login(TEST_USER_NAME, TEST_PASSWORD, null));
+        verify(dashboardUserMapper, times(5)).findByQuery(eq(TEST_USER_NAME), 
anyString());
+
     }
 
     private DashboardUserDO createDashboardUserDO() {
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/SecretServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/SecretServiceTest.java
new file mode 100644
index 0000000000..0840f89099
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/SecretServiceTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.shenyu.admin.config.properties.SecretProperties;
+import org.apache.shenyu.admin.service.impl.SecretServiceImpl;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+/**
+ * Test for {@link SecretServiceImpl}.
+ */
+public class SecretServiceTest {
+
+    private static final String TEST_AES_KEY = "2095132720951327";
+
+    private static final String TEST_AES_IV = "6075877187097700";
+
+    @Test
+    public void infoShouldNotExposeConfiguredSecrets() throws Exception {
+        SecretProperties secretProperties = new SecretProperties();
+        secretProperties.setKey(TEST_AES_KEY);
+        secretProperties.setIv(TEST_AES_IV);
+
+        SecretService secretService = new SecretServiceImpl();
+        String encoded = secretService.info();
+        String decoded = new String(Base64.getDecoder().decode(encoded), 
StandardCharsets.UTF_8);
+        @SuppressWarnings("unchecked")
+        Map<String, String> secretInfo = new ObjectMapper().readValue(decoded, 
Map.class);
+
+        assertNotEquals(TEST_AES_KEY, secretInfo.get("key"));
+        assertNotEquals(TEST_AES_IV, secretInfo.get("iv"));
+        assertEquals("", secretInfo.get("key"));
+        assertEquals("", secretInfo.get("iv"));
+    }
+}

Reply via email to