第52批: 修复\/JSON转义脏值PHP侧(清洗函数+比对容错+存量自愈)

- core.php新增b64Clean(base64白名单清洗)与cleanB64DirtyOnce(存量清洗,自备.b64clean标记挂migrateDatabase锁前)
- 比对点接入b64Clean: core.php临时/主密码验证, account.php第一层比对与改密回读, sync.php改密旧密码验证
- 根因: PHP json_encode默认把base64中的/转义成\/,Java历史解析层漏还原写进库,hash脏值致两端密码恒错; 本侧为第52批四层防护的PHP半边
This commit is contained in:
ypsdf1
2026-10-23 19:21:26 +08:00
parent 4acb6712cd
commit 3a4252e861
3 changed files with 39 additions and 5 deletions
+2 -2
View File
@@ -317,7 +317,7 @@ function acctPwdCheck($cred, $pwd) {
$saltBytes = saltDecode((string)$cred['salt']); // ★第48批:容错解盐(core.php saltDecode)
if ($saltBytes === false || $saltBytes === '') return false;
$calc = base64_encode(hash('sha256', $saltBytes . $pwd, true));
return hash_equals((string)$cred['password_hash'], $calc);
return hash_equals(b64Clean((string)$cred['password_hash']), $calc); // ★第52批:存量 \/ 脏值容错
}
function acctEmailOf($player) {
@@ -717,7 +717,7 @@ function acctChangePassword() {
// 回读核对(不依赖 changes(),SQLite/MySQL 双模式同形)
$after = acctCredentials($player);
if (!$after || !hash_equals((string)$after['password_hash'], $newHash)) {
if (!$after || !hash_equals(b64Clean((string)$after['password_hash']), $newHash)) { // ★第52批:回读值清洗后比对
debugLog('[account.php] 改密写入核对失败', ['player' => $player, 'ip' => acctClientIp()]);
error('密码写入失败,请重试', 500);
}
+1 -1
View File
@@ -4864,7 +4864,7 @@ function changePassword() {
}
$oldHashComputed = base64_encode(hash('sha256', $saltBytes . $oldPwd, true));
if ($oldHashComputed !== $row['password_hash']) {
if ($oldHashComputed !== b64Clean($row['password_hash'])) { // ★第52批:存量 \/ 脏值容错
error('当前密码错误');
}
+36 -2
View File
@@ -1776,6 +1776,8 @@ function walCheckpoint($db = null) {
*/
function migrateDatabase($db, $side = null) {
if ($side !== null) { $db = new SdfDbSideFilter($db, $side); }
// ★ 第52批:存量脏凭证清洗挂在 .migrated 锁短路之前(自备 .b64clean 锁,首次 O(1) 短路)
cleanB64DirtyOnce($db, $side);
// 检查是否已迁移(用文件标记,避免每次都做 PRAGMA 查询)
$migrateLock = sqlDbFile($side === 'java' ? 'java' : 'web') . '.migrated';
if (file_exists($migrateLock)) {
@@ -3073,6 +3075,37 @@ function saltDecode($salt) {
return $bytes;
}
/**
* ★ 第52批:base64 凭证白名单清洗。
* PHP json_encode 默认把 base64 中的 / 转义成 \/,Java 历史解析层漏还原
* 曾把反斜杠写进 login.db / weblogin_credentials(hash 脏值 → 两端密码恒错)。
* base64 合法字符永不含反斜杠,先还原 \/ 再按白名单清洗即得正确值;
* 比对两侧同款清洗,保证对称。
*/
function b64Clean($v) {
$v = str_replace('\\/', '/', (string)$v);
return preg_replace('/[^A-Za-z0-9+\/=]/', '', $v);
}
/**
* ★ 第52批:存量脏凭证一次性清洗(\/ JSON 转义残留)。
* 挂在 migrateDatabase 锁短路之前、自备 .b64clean 标记文件:
* 首次请求全表清洗一遍(WHERE instr 命中才改行),之后 O(1) 短路;
* 表未建时不写标记,下次请求重试。
*/
function cleanB64DirtyOnce($db, $side = null) {
$lock = sqlDbFile($side === 'java' ? 'java' : 'web') . '.b64clean';
if (file_exists($lock)) return;
try {
$db->exec("UPDATE weblogin_credentials SET password_hash=REPLACE(password_hash, char(92), '') WHERE instr(password_hash, char(92))>0");
$db->exec("UPDATE weblogin_credentials SET salt=REPLACE(salt, char(92), '') WHERE instr(salt, char(92))>0");
$db->exec("UPDATE weblogin_credentials SET temp_password_hash=REPLACE(temp_password_hash, char(92), '') WHERE instr(temp_password_hash, char(92))>0");
file_put_contents($lock, time());
} catch (Exception $e) {
// 表可能尚未创建(initTables 会建),不写标记,下次重试
}
}
/**
* 验证Web登录密码(优先校验临时密码,再校验主密码)
* 返回: 'temp' 表示临时密码, 'main' 表示主密码, false 表示失败
@@ -3104,7 +3137,8 @@ function verifyWebLoginPassword($playerName, $password) {
if ($saltBytes !== false && $saltBytes !== '') {
$rawHash = hash('sha256', $saltBytes . $password, true);
$computedHash = base64_encode($rawHash);
if ($computedHash === $row['temp_password_hash']) {
// ★ 第52批:库值比对前白名单清洗(存量 \/ 脏值容错)
if ($computedHash === b64Clean($row['temp_password_hash'])) {
return 'temp'; // 临时密码匹配
}
}
@@ -3112,7 +3146,7 @@ function verifyWebLoginPassword($playerName, $password) {
}
// 2. 再检查主密码
$storedHash = $row['password_hash'];
$storedHash = b64Clean($row['password_hash']); // ★ 第52批:存量 \/ 脏值容错(比对前清洗)
$salt = $row['salt'];
$saltBytes = saltDecode($salt); // ★第48批:容错解盐(镜像Java清洗)
if ($saltBytes === false || $saltBytes === '') return false;