diff --git a/admin_2fa_setup.php b/admin_2fa_setup.php index 36a4319..9105284 100644 --- a/admin_2fa_setup.php +++ b/admin_2fa_setup.php @@ -17,6 +17,10 @@ secEnsureSession(); secGatePage(true); $boot = secBootSessionActive(); +// ★ 首次部署:管理密码还是分发占位符 → 走初始化模式。 +// 旧行为在这里直接跳登录页,是个死胡同:登录要密码,而这时候根本没有可用密码。 +$needInit = !secAdminPassConfigured(); + // ===== 访问模式判定 ===== // 说明:本页只依赖 security.php(不加载 core.php), // isAdminLoggedIn() 是 core 的函数,这里用等价的内联判定(admin_auth + 安全纪元)。 @@ -30,6 +34,8 @@ if ($boot) { exit; } $mode = 'session'; +} elseif ($needInit) { + $mode = 'init'; } else { header('Location: admin_login.php'); exit; @@ -49,6 +55,11 @@ $pending = isset($_SESSION['sec_pending_secret']) ? (string)$_SESSION['sec_pendi $myIp = secClientIp(); $err = ''; $ok = ''; +// 初始化成功后 302 回本页刷新(令牌要落盘后重读才准),提示语靠 session 带过来 +if (!empty($_SESSION['sec_flash_ok'])) { + $ok = (string)$_SESSION['sec_flash_ok']; + unset($_SESSION['sec_flash_ok']); +} // ===== POST:所有写操作 ===== if ($_SERVER['REQUEST_METHOD'] === 'POST' && $mode !== 'none') { @@ -57,6 +68,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $mode !== 'none') { if ($mode === 'boot') { $allowed = true; // 引导模式(已持有服务器文件级凭据)免密码 + } elseif ($act === 'init_setup') { + // ★ 首次初始化:这一步要设的就是密码本身,没有密码可校验。 + // 真正的闸门是 $needInit —— 密码一旦配置成功,这条分支立刻关死。 + if (!$needInit) { + $err = '管理密码已配置,初始化只能进行一次'; + } else { + $allowed = true; + } } else { // 第二层附加保护:改配置必须重输管理密码(防活会话被盗用者直接改安全配置) $lock = secThrottleLocked('setup_pw', $myIp, 5, 600); @@ -65,7 +84,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $mode !== 'none') { secLog('setup_throttled', 'lock=' . $lock . 's'); } else { $pw = isset($_POST['admin_pass']) ? (string)$_POST['admin_pass'] : ''; - if ($pw !== '' && $pw === ADMIN_PASS) { + // 顺带关死"占位符当密码"这条路:密码没配置时 session 模式一律不放行 + if ($pw !== '' && secAdminPassConfigured() && $pw === ADMIN_PASS) { $allowed = true; secThrottleReset('setup_pw', $myIp); } else { @@ -95,23 +115,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $mode !== 'none') { ), $over); }; - if ($act === 'save_whitelist') { - $lines = preg_split('/\r\n|\r|\n/', isset($_POST['whitelist']) ? (string)$_POST['whitelist'] : ''); - $items = array(); - foreach ($lines as $L) { - $L = trim($L); - if ($L === '') continue; - if (filter_var($L, FILTER_VALIDATE_IP)) { - $items[] = $L; - } elseif (preg_match('#^(\d{1,3}\.){3}\d{1,3}/(\d{1,2})$#', $L, $mm) - && (int)$mm[2] <= 32 - && filter_var(explode('/', $L)[0], FILTER_VALIDATE_IP)) { - $items[] = $L; - } else { - throw new Exception('无效的 IP / CIDR:' . htmlspecialchars($L, ENT_QUOTES, 'UTF-8')); - } + if ($act === 'init_setup') { + // ===== 首次初始化:设置管理密码 + 写入白名单,一次做完 ===== + if (!$needInit) throw new Exception('管理密码已配置,初始化只能进行一次'); + $pw1 = isset($_POST['new_pass']) ? (string)$_POST['new_pass'] : ''; + $ve = secValidateNewPassword($pw1, isset($_POST['new_pass2']) ? (string)$_POST['new_pass2'] : ''); + if ($ve !== null) throw new Exception($ve); + + $items = secParseWhitelistText(isset($_POST['whitelist']) ? (string)$_POST['whitelist'] : ''); + // 自锁保护:初始化页还没展示过任何恢复地址,白名单里漏掉自己的 IP + // 就会保存完立刻 404、且不知道 boot 令牌是什么 → 这里直接拦下来。 + if (!secIpInWhitelist($myIp, $items)) { + throw new Exception('白名单里没有你当前的 IP(' . htmlspecialchars($myIp, ENT_QUOTES, 'UTF-8') + . '),保存后你会立刻把自己挡在门外。请把它加进去,或改成覆盖你网段的 CIDR。'); } - $items = array_values(array_unique($items)); + $twoFaSnap = secReadUpdateBlock(); + // ① 先改写 config 第 32 行那条 ADMIN_PASS,再写 SEC-UPDATE 区 + // (白名单 + 自动生成的两个令牌 + 安全纪元,顺手打引导标记)。 + // 任一步失败都抛给下面的 catch 显示,可直接重试、不会把人锁死: + // 密码没设成 → 还是初始化页;密码设成而区段没写成 → 用新密码登录回来接着配。 + secWriteAdminPass($pw1); + secWriteConfig($full(array('ADMIN_IP_WHITELIST' => $items))); + secGuardTwoFaUnchanged($twoFaSnap, 'init_setup'); + $whitelist = $items; + secLog('admin_init', 'whitelist=' . count($items) . ' ' . implode(',', $items)); + + // ② 自动登录,省得刚设完密码又敲一遍 + secEnsureSession(); + $_SESSION['admin_auth'] = true; + $_SESSION['admin_login_time'] = time(); + unset($_SESSION['admin_2fa_ok'], $_SESSION['admin_2fa_ip']); + + // ③ 回本页刷新:两个令牌是这次才落盘的,当前请求里的 $secToken 还是 + // 刚生成、没保存过的随机值,直接渲染出来会骗人(保存后就对不上了)。 + $_SESSION['sec_flash_ok'] = '初始化完成:管理密码已设置,IP 白名单已保存(' . count($items) + . ' 条),门禁已转为强制模式。请立刻收藏本页显示的后台入口地址。'; + header('Location: admin_2fa_setup.php'); + exit; + } elseif ($act === 'save_whitelist') { + $items = secParseWhitelistText(isset($_POST['whitelist']) ? (string)$_POST['whitelist'] : ''); // 改白名单同样不许碰 2FA:写前快照 + 写后护栏(同 rotate_token) $twoFaSnap = secReadUpdateBlock(); secWriteConfig($full(array('ADMIN_IP_WHITELIST' => $items))); @@ -290,7 +332,8 @@ if (file_exists($secLogPath)) {