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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 0138c0726 fix(auth): burn a dummy hash on unknown usernames to close 
the login timing side channel (#5134)
0138c0726 is described below

commit 0138c0726ed0a46dfd2cb3eb364f8e72b25f0216
Author: cyberslack_lee <[email protected]>
AuthorDate: Thu Oct 1 18:12:02 2026 +0800

    fix(auth): burn a dummy hash on unknown usernames to close the login timing 
side channel (#5134)
    
    Signed-off-by: enkilee <[email protected]>
---
 .../apache/rocketmq/studio/auth/AuthService.java   | 12 +++++++--
 .../studio/auth/AuthServiceDatabaseTest.java       | 29 ++++++++++++++++++++++
 2 files changed, 39 insertions(+), 2 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
index 2c13ec58b..782608abd 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
@@ -382,8 +382,16 @@ public class AuthService {
 
     private LoginVO loginDatabaseUser(LoginDTO request) {
         ensureBootstrapUsers();
-        RmqStudioUser user = findUserByUsername(request.getUsername())
-                .orElseThrow(() -> new BusinessException(401, "Invalid 
username or password"));
+        Optional<RmqStudioUser> found = 
findUserByUsername(request.getUsername());
+        if (found.isEmpty()) {
+            // Burn one dummy derivation so the response timing matches the 
wrong-password path
+            // on an existing account. Without this, an attacker could 
distinguish "user not
+            // found" (fast) from "user found but wrong password" (slow 
PBKDF2) and enumerate
+            // valid usernames by measuring response time.
+            passwordHasher.matches(request.getPassword(), DUMMY_PASSWORD_HASH);
+            throw new BusinessException(401, "Invalid username or password");
+        }
+        RmqStudioUser user = found.get();
         if (!Boolean.TRUE.equals(user.getEnabled())) {
             // Answer exactly like a wrong password on an enabled account: 
burn one dummy
             // derivation so the response timing matches, and never touch this 
account's
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceDatabaseTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceDatabaseTest.java
index 9e0a43c9f..dd993bb8e 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceDatabaseTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceDatabaseTest.java
@@ -511,6 +511,35 @@ class AuthServiceDatabaseTest {
                 .hasMessage("Invalid username or password");
     }
 
+    @Test
+    void unknownUserLoginBurnsDummyHashToPreventTimingSideChannelTest() {
+        PasswordHasher hasherSpy = mock(PasswordHasher.class);
+        SettingsRepository repository = mock(SettingsRepository.class);
+        when(repository.loadGeneralSettings())
+                
.thenReturn(GeneralSettingsVO.builder().sessionTimeout(30).build());
+        authService = new AuthService(new AuthProperties(), repository,
+                Clock.fixed(Instant.parse("2026-08-13T00:00:00Z"), 
ZoneOffset.UTC), userMapper,
+                sessionMapper, hasherSpy);
+        when(userMapper.selectCount(isNull())).thenReturn(1L);
+        when(userMapper.selectOne(any(Wrapper.class))).thenReturn(null);
+
+        LoginDTO request = new LoginDTO();
+        request.setUsername("no-such-user");
+        request.setPassword("any-password");
+
+        assertThatThrownBy(() -> authService.login(request))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(exception ->
+                        assertThat(((BusinessException) 
exception).getCode()).isEqualTo(401))
+                .hasMessage("Invalid username or password");
+        // The dummy PBKDF2 derivation must run even when the user is not 
found, so the
+        // response timing is indistinguishable from a wrong-password attempt 
on a real
+        // account. Without this, an attacker could enumerate valid usernames 
by measuring
+        // the response time difference between "user not found" and "wrong 
password".
+        verify(hasherSpy, times(1)).matches(anyString(), argThat(hash ->
+                hash != null && hash.startsWith("pbkdf2$210000$")));
+    }
+
     @Test
     void 
enabledAccountsWithWrongPasswordsGetTheUniformInvalidCredentialsResponse() {
         when(userMapper.selectCount(isNull())).thenReturn(1L);

Reply via email to