From 77a4cd4e7d1e8df715c150b4298d8415d5d61fa2 Mon Sep 17 00:00:00 2001 From: Ling Bao Date: Tue, 7 Jul 2026 11:22:03 +1000 Subject: [PATCH] fix: harden login account duplicate-row reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 36 domain-key duplicate-row hardening. Verification: - [x] `mvn test -Dtest=LoginServiceTest` — 122 tests, 0 failures, 0 errors\n- [x] `mvn test` — 459 tests, 0 failures, 0 errors gate_status: skill --- .../plugins/login/service/LoginService.java | 9 ++++++++- .../plugins/login/service/LoginServiceTest.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ultikits/plugins/login/service/LoginService.java b/src/main/java/com/ultikits/plugins/login/service/LoginService.java index 9760b74..fea81d4 100644 --- a/src/main/java/com/ultikits/plugins/login/service/LoginService.java +++ b/src/main/java/com/ultikits/plugins/login/service/LoginService.java @@ -24,6 +24,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -605,7 +606,13 @@ public AccountData getAccount(UUID playerUuid) { List accounts = dataOperator.query() .where("player_uuid").eq(playerUuid.toString()) .list(); - return accounts.isEmpty() ? null : accounts.get(0); + return accounts.isEmpty() ? null : selectCanonicalAccount(accounts); + } + + private AccountData selectCanonicalAccount(List accounts) { + return accounts.stream() + .min(Comparator.comparing(AccountData::getId, Comparator.nullsLast(String::compareTo))) + .orElse(accounts.get(0)); } /** diff --git a/src/test/java/com/ultikits/plugins/login/service/LoginServiceTest.java b/src/test/java/com/ultikits/plugins/login/service/LoginServiceTest.java index 0a3df61..72a24e4 100644 --- a/src/test/java/com/ultikits/plugins/login/service/LoginServiceTest.java +++ b/src/test/java/com/ultikits/plugins/login/service/LoginServiceTest.java @@ -550,6 +550,21 @@ void accountExists() { assertThat(result).isSameAs(expected); } + @Test + @DisplayName("Should select canonical lowest-id account when duplicate player UUID rows exist") + void duplicatePlayerUuidRowsSelectLowestIdAccount() { + AccountData newer = UltiLoginTestHelper.createSampleAccount(playerUuid, "Newer", "hash2", "salt2"); + newer.setId("account-200"); + AccountData canonical = UltiLoginTestHelper.createSampleAccount(playerUuid, "Canonical", "hash1", "salt1"); + canonical.setId("account-100"); + when(mockQuery.list()) + .thenReturn(Arrays.asList(newer, canonical)); + + AccountData result = service.getAccount(playerUuid); + + assertThat(result).isSameAs(canonical); + } + @Test @DisplayName("Should return null when doesn't exist") void noAccount() {