第52批: 修复\/JSON转义脏值四层防护
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m34s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

- 解析根修: addParsedEntry/parseJsonArrayItem清洗链补\/还原(置于\\还原之前); extractJsonString扫描跳过转义序列并按JSON规则反转义
- 写库兜底: createUser/updatePassword入参b64Clean; applyPulledRow credentials的hash/salt/temp写库前白名单清洗
- 存量迁移: init启动时REPLACE char92清洗users表password_hash/password_salt/temp_password(仿一次性自愈范式,WHERE instr条件幂等)
- 读取容错: checkPassword/checkPasswordWithFallback/checkPasswordForChange比对前双侧b64Clean
- 根因: PHP json_encode把base64中的/转义成\/,历史解析层漏还原写进库,hash脏值致游戏端+网页端密码恒错
This commit is contained in:
youpaishidifu
2026-10-23 19:21:14 +08:00
parent 5b705d0af2
commit 474b912afe
2 changed files with 60 additions and 8 deletions
+34 -3
View File
@@ -150,6 +150,20 @@ public class DatabaseManager {
} catch (SQLException ignored) {
}
// ★ 第52批:存量脏凭证一次性清洗——PHP json_encode 把 base64 中的
// / 转义成 \/,历史解析层漏还原曾把反斜杠写进库(hash 脏 → 密码恒错)。
// base64 合法字符永不含反斜杠(char92),删除即还原正确值;WHERE 带
// instr 条件,无脏值时为近似 no-op,可每次启动安全重跑。
try {
st.executeUpdate("UPDATE users SET password_hash=REPLACE(password_hash, char(92), '') "
+ "WHERE instr(password_hash, char(92))>0");
st.executeUpdate("UPDATE users SET password_salt=REPLACE(password_salt, char(92), '') "
+ "WHERE instr(password_salt, char(92))>0");
st.executeUpdate("UPDATE users SET temp_password=REPLACE(temp_password, char(92), '') "
+ "WHERE instr(temp_password, char(92))>0");
} catch (SQLException ignored) {
}
// ===== 背包备份表:安全迁移 =====
// 检查旧表是否存在(用 inventory_data 列判断)
boolean hasOldTable = false;
@@ -748,8 +762,22 @@ public class DatabaseManager {
}
}
/**
* ★ 第52批:base64 凭证白名单清洗。
* PHP json_encode 默认把 base64 中的 / 转义成 \/,历史解析层漏还原
* 会把反斜杠写进库(hash 脏值 → 游戏/网页两端密码恒错)。
* base64 合法字符永不含反斜杠,先还原 \/ 再按白名单清洗即得正确值;
* 写库与比对两侧同款清洗,保证对称。
*/
public static String b64Clean(String v) {
if (v == null) return null;
return v.replace("\\/", "/").replaceAll("[^A-Za-z0-9+/=]", "");
}
public void createUser(String name, String hash,
String salt) {
hash = b64Clean(hash);
salt = b64Clean(salt);
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO users "
@@ -774,6 +802,8 @@ public class DatabaseManager {
* 更新玩家密码凭证(salt + hash),用于Web重新注册时同步
*/
public void updatePassword(String name, String hash, String salt) {
hash = b64Clean(hash);
salt = b64Clean(salt);
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE users SET password_hash = ?, password_salt = ? WHERE player_name = ?");
@@ -835,7 +865,8 @@ public class DatabaseManager {
rs.getString("password_hash");
rs.close();
ps.close();
return hash.equals(mainHash);
// ★ 第52批:比对前双侧清洗(存量脏 hash 容错,见 b64Clean)
return b64Clean(hash).equals(b64Clean(mainHash));
} else {
rs.close();
ps.close();
@@ -874,7 +905,7 @@ public class DatabaseManager {
ps.close();
if (tempHash != null
&& !tempHash.isEmpty()
&& mainHash.equals(tempHash)
&& b64Clean(mainHash).equals(b64Clean(tempHash))
&& System.currentTimeMillis() < expire
&& used != 1) {
return "temp";
@@ -923,7 +954,7 @@ public class DatabaseManager {
ps.close();
if (tempHash != null
&& !tempHash.isEmpty()
&& hash.equals(tempHash)
&& b64Clean(hash).equals(b64Clean(tempHash))
&& used != 1) {
return "temp";
}
+26 -5
View File
@@ -4222,7 +4222,10 @@ public class WebManager {
}
// 字符串(去掉引号)
// ★ 第52批:\/ 必须先于 \\ 还原——PHP json_encode 默认把 base64 中的
// / 转义成 \/,历史解析层漏还原会把反斜杠写进 login.db(hash 脏值 → 密码恒错)。
String cleaned = value.replaceAll("^\"|\"$", "")
.replace("\\/", "/")
.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\\\", "\\");
@@ -4290,7 +4293,9 @@ public class WebManager {
return parseJson(item);
}
if (item.startsWith("\"") && item.endsWith("\"")) {
// ★ 第52批:\/ 先于 \\ 还原(PHP json_encode 的 base64 斜杠转义)
return item.substring(1, item.length() - 1)
.replace("\\/", "/")
.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\\\", "\\");
@@ -4827,8 +4832,9 @@ public class WebManager {
DatabaseManager dbMgr = plugin.getDb();
if (dbMgr == null) return false;
String name = strOf(row.get("player_name"));
String hash = strOf(row.get("password_hash"));
String salt = strOf(row.get("password_salt"));
// ★ 第52批:写库前白名单清洗(PHP json_encode 的 \/ 转义残留兜底)
String hash = DatabaseManager.b64Clean(strOf(row.get("password_hash")));
String salt = DatabaseManager.b64Clean(strOf(row.get("password_salt")));
if (name.isEmpty() || hash.isEmpty() || salt.isEmpty()) return false;
// [凭证诊断49] 收回写库前抓本地旧值,写库后回读,完整新旧值入日志抓 ping-pong 现行
boolean ex49 = dbMgr.userExists(name);
@@ -4851,7 +4857,7 @@ public class WebManager {
plugin.getLogger().info("[凭证诊断49] 收回写库(无变化) name=" + name
+ " hash=" + newH49 + " salt=" + newS49);
}
String tmpHash = strOf(row.get("temp_password"));
String tmpHash = DatabaseManager.b64Clean(strOf(row.get("temp_password")));
long tmpExpire = longOf(row.get("temp_pw_expire"));
if (!tmpHash.isEmpty() && tmpExpire > 0) {
dbMgr.setField(name, "temp_password", tmpHash);
@@ -11469,9 +11475,24 @@ public class WebManager {
int start = json.indexOf(search);
if (start == -1) return null;
start += search.length();
int end = json.indexOf("\"", start);
// ★ 第52批:扫描跳过转义序列(\" \/ \\ 等),避免值内转义引号提前截断;
// 取到原文后按 JSON 规则反转义,\/ 先于 \\ 还原(PHP json_encode 的
// base64 斜杠转义),否则 password_hash/salt 会带反斜杠直接入库。
int i = start;
int end = -1;
while (i < json.length()) {
char c = json.charAt(i);
if (c == '\\' && i + 1 < json.length()) { i += 2; continue; }
if (c == '"') { end = i; break; }
i++;
}
if (end == -1) return null;
return json.substring(start, end);
String raw = json.substring(start, end);
return raw.replace("\\/", "/")
.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\\\", "\\");
}
// ==================== Web注册请求轮询 ====================