fix(security): 安全漏洞修复与依赖升级

- 升级 Vert.x 4.5.24 → 4.5.27, postgresql 42.7.3 → 42.7.11, logback 1.5.18 → 1.5.32, axios 1.13.5 → 1.16.1
- 修复 JWT 签名验证和密码比较的时序攻击漏洞 (MessageDigest.isEqual)
- 修复 AESUtils 使用不安全 Random 改为 SecureRandom
- 修复登录用户枚举和异常信息泄露,统一错误提示
- 修复 RateLimiter count++ 非原子操作 (AtomicInteger)
- 修复 JsParserExecutor DCL 模式缺少 volatile
- 修复 Token 日志泄露,仅打印前8字符
- 修复 Playground 密码时序攻击和堆栈泄露
- 所有 window.open 添加 noopener,noreferrer
- LocalConstant 改用 ConcurrentHashMap 保证线程安全
- Dockerfile 添加非 root 用户运行,secret.yml 加入 .gitignore
This commit is contained in:
yukaidi
2026-05-29 14:20:54 +08:00
parent ff400d3be3
commit 17460ff271
22 changed files with 212 additions and 155 deletions

View File

@@ -10,6 +10,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class RateLimiter {
@@ -51,12 +52,12 @@ public class RateLimiter {
return new RequestInfo(1, currentTime);
} else {
// 增加计数器
requestInfo.count++;
requestInfo.count.incrementAndGet();
return requestInfo;
}
});
if (info.count > MAX_REQUESTS) {
if (info.count.get() > MAX_REQUESTS) {
// 超过限制
// 计算剩余时间
long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp);
@@ -71,11 +72,11 @@ public class RateLimiter {
}
private static class RequestInfo {
volatile int count;
final AtomicInteger count;
volatile long timestamp;
RequestInfo(int count, long time) {
this.count = count;
this.count = new AtomicInteger(count);
this.timestamp = time;
}
}

View File

@@ -7,6 +7,7 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -93,9 +94,10 @@ public class JwtUtil {
String encodedPayload = parts[1];
String signature = parts[2];
// 验证签名
// 验证签名(使用常量时间比较防止时序攻击)
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
if (!expectedSignature.equals(signature)) {
if (!MessageDigest.isEqual(expectedSignature.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8))) {
return false;
}

View File

@@ -80,8 +80,10 @@ public class PasswordUtil {
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash);
// 比较计算出的哈希值和存储的哈希值
return hashBase64.equals(calculatedHashBase64);
// 比较计算出的哈希值和存储的哈希值(使用常量时间比较防止时序攻击)
return MessageDigest.isEqual(
hashBase64.getBytes(StandardCharsets.UTF_8),
calculatedHashBase64.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
// 如果发生异常例如格式不正确返回false
return false;

View File

@@ -28,6 +28,7 @@ import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
@@ -129,8 +130,11 @@ public class PlaygroundApi {
return promise.future();
}
// 验证密码
if (config.getPassword().equals(password)) {
// 验证密码(使用常量时间比较防止时序攻击)
String storedPassword = config.getPassword();
if (storedPassword != null && MessageDigest.isEqual(
storedPassword.getBytes(StandardCharsets.UTF_8),
password.getBytes(StandardCharsets.UTF_8))) {
String token = config.generateToken();
JsonObject tokenData = new JsonObject().put("token", token);
promise.complete(JsonResult.data(tokenData).toJsonObject());
@@ -299,7 +303,6 @@ public class PlaygroundApi {
}).onFailure(e -> {
long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场执行失败", e);
@@ -317,7 +320,6 @@ public class PlaygroundApi {
PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false)
.error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime)
.logs(respLogs)
.build();
@@ -328,14 +330,12 @@ public class PlaygroundApi {
} catch (Exception e) {
long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场初始化失败", e);
PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false)
.error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime)
.logs(new ArrayList<>())
.build();
@@ -346,8 +346,7 @@ public class PlaygroundApi {
log.error("解析请求参数失败", e);
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("解析请求参数失败: " + e.getMessage())
.stackTrace(getStackTrace(e))
.error("解析请求参数失败")
.build()));
}
@@ -696,18 +695,5 @@ public class PlaygroundApi {
}
return ip;
}
/**
* 获取异常堆栈信息
*/
private String getStackTrace(Throwable throwable) {
if (throwable == null) {
return "";
}
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
throwable.printStackTrace(pw);
return sw.toString();
}
}

View File

@@ -125,18 +125,18 @@ public class UserServiceImpl implements UserService {
if (rows.size() == 0) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
.put("message", "用户名或密码错误"));
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
// 验证密码
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "密码错误"));
.put("message", "用户名或密码错误"));
return;
}
@@ -169,7 +169,7 @@ public class UserServiceImpl implements UserService {
log.error("登录查询失败", err);
promise.complete(new JsonObject()
.put("success", false)
.put("message", "登录失败: " + err.getMessage()));
.put("message", "登录失败,请稍后重试"));
});
return promise.future();
@@ -189,10 +189,10 @@ public class UserServiceImpl implements UserService {
.execute(Tuple.of(username))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
promise.fail("用户名或密码错误");
return;
}
Row row = rows.iterator().next();
SysUser user = rowToUser(row);
promise.complete(filterSensitiveInfo(user));
@@ -296,10 +296,10 @@ public class UserServiceImpl implements UserService {
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
promise.fail("用户名或密码错误");
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
@@ -406,7 +406,7 @@ public class UserServiceImpl implements UserService {
.onFailure(err -> {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
.put("message", "认证失败,请重新登录"));
});
return promise.future();