紧急加固三层:admin.php 入口令牌门禁 + 2FA 扫码绑定与精确诊断
第三层(新增,config.php 的 SEC_ACCESS_TOKEN): - 只有 admin.php?token=<令牌> 才放行,缺/错令牌一律返回 nginx 原生 404 (与"文件真的不存在"逐字节一致,探测者无从判断),令牌检查置于白名单之前 - 登录接口在密码校验通过后才下发 token,2FA 通过与收银台跳转由服务端补拼 - 入口页加 Referrer-Policy: no-referrer,防令牌经外链 Referer 泄露 - 配置页展示后台唯一入口地址,支持一键重新生成令牌 第一层修复(2FA 验证码"无效"): - secTotpVerify 容错窗口 ±30 秒扩到 ±90 秒 - 新增 secTotpDiagnose:失败时区分"密钥没对上"与"设备时钟偏移多少秒"并给出处置指引 - Base32 解码兼容空格/连字符与 0/1/8/9 手误,避免密钥被静默截短 - 验证页倒计时与服务器时间显示改用服务端时钟基准 - 密钥生成改幂等,"重新生成"二次确认,防止已扫码密钥被误作废 第二层(二维码): - admin_2fa_setup.php 内联 qrcode-generator(MIT) 生成 otpauth 二维码,离线可用 - 后台侧边栏新增"安全配置"入口;admin_2fa_setup.php?boot= 在白名单 IP 下同样生效 已通过线上端到端验证 22 项 + 二维码 OpenCV 往返解码校验。
This commit is contained in:
+36
-2
@@ -41,6 +41,10 @@ if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// ★ 紧急加固(2026-09-29):第二层 IP 白名单 —— 白名单未配置时,
|
||||
// 任何外部 IP 连 login/status 也一律 403(127.0.0.1 本机永放行)
|
||||
secGateApi();
|
||||
|
||||
$action = getParam('action', 'status');
|
||||
|
||||
try {
|
||||
@@ -89,10 +93,33 @@ switch ($action) {
|
||||
|
||||
// ===== 各函数定义 =====
|
||||
function adminDoLogin() {
|
||||
// ★ 紧急加固:同 IP 暴力破解限速(5 次失败锁定 10 分钟),失败写审计日志
|
||||
$ip = function_exists('secClientIp') ? secClientIp() : '';
|
||||
if (function_exists('secThrottleLocked')) {
|
||||
$lock = secThrottleLocked('login_fail', $ip, 5, 600);
|
||||
if ($lock > 0) {
|
||||
if (function_exists('secLog')) secLog('login_throttled', 'lock=' . $lock . 's');
|
||||
exit(json_encode(['success' => false, 'message' => '尝试次数过多,请 ' . $lock . ' 秒后再试', 'code' => 429], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
$password = getParam('password');
|
||||
if (!$password) exit(json_encode(['success' => false, 'message' => 'Missing password'], JSON_UNESCAPED_UNICODE));
|
||||
if (adminLogin($password)) {
|
||||
exit(json_encode(['success' => true, 'data' => ['login_time' => time()], 'message' => 'OK'], JSON_UNESCAPED_UNICODE));
|
||||
if (function_exists('secThrottleReset')) secThrottleReset('login_fail', $ip);
|
||||
if (function_exists('secLog')) secLog('login_pass', 'password_ok');
|
||||
// ★ 第一层:已启用二次验证时,告知前端跳转 6 位动态码验证页
|
||||
$need2fa = function_exists('secSessionNeed2FA') && secSessionNeed2FA();
|
||||
// ★ 第三层:登录成功后把后台入口令牌交给前端,用于拼 admin.php?token=...
|
||||
// —— 令牌只在密码正确之后才出现,登录页 HTML 里不含它。
|
||||
$tok = function_exists('secToken') ? secToken() : '';
|
||||
exit(json_encode(['success' => true, 'data' => ['login_time' => time(), 'need_2fa' => $need2fa, 'token' => $tok], 'message' => 'OK'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
if (function_exists('secThrottleFail')) {
|
||||
$left = secThrottleFail('login_fail', $ip, 5, 600);
|
||||
if (function_exists('secLog')) secLog('login_fail', 'bad_password' . ($left > 0 ? ' LOCKED_' . $left . 's' : ''));
|
||||
if ($left > 0) {
|
||||
exit(json_encode(['success' => false, 'message' => '尝试次数过多,请 ' . $left . ' 秒后再试', 'code' => 429], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
exit(json_encode(['success' => false, 'message' => '密码错误', 'code' => 401], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
@@ -103,9 +130,16 @@ function adminDoLogout() {
|
||||
}
|
||||
|
||||
function adminStatus() {
|
||||
$in = function_exists('isAdminLoggedIn') ? isAdminLoggedIn() : false;
|
||||
// ★ 第一层:前端据 need_2fa 决定跳验证页还是直进后台
|
||||
$need2fa = $in && function_exists('secSessionNeed2FA') && secSessionNeed2FA();
|
||||
exit(json_encode([
|
||||
'success' => true,
|
||||
'data' => ['logged_in' => isAdminLoggedIn(), 'login_time' => $_SESSION['admin_login_time'] ?? 0],
|
||||
'data' => [
|
||||
'logged_in' => $in,
|
||||
'login_time' => $_SESSION['admin_login_time'] ?? 0,
|
||||
'need_2fa' => $need2fa
|
||||
],
|
||||
'message' => 'ok'
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user