boot恢复模式不再404死胡同:持boot凭据的请求一律302回引导配置页
- security.php 新增 secBootRecoveryRedirect():secGatePage 遇到「不在 IP 白名单但持有效 boot 凭据」的请求,改 302 送回 admin_2fa_setup.php(唯一豁免页),不再返回 nginx 404;secBootAuthenticate 增加凭据预判(无 boot 参数、无会话 cookie、无活动会话则直接判失败),避免路由覆盖面变大后给每个探测请求都 session_start() 刷 session 文件。 - admin.php 令牌层改 secTokenGate(true):admin.php?boot=<引导令牌> 才能进入恢复路由(原先在令牌层就 404);页面对非白名单 IP 依旧不渲染。 - admin_2fa.php 持 boot 但不在白名单 → 直接送回配置页,不再掉进 admin_login.php / admin.php?token= 的 404 弹簧。 - admin_2fa_setup.php:boot 模式隐藏「← 返回」、顶部加恢复指引横幅、后台唯一入口下加 404 警告;新增 修正「刚保存白名单但 ADMIN_IP_WHITELIST 常量本请求内未刷新」导致横幅误报。 - 背景(db/security.log 实录):2026-10-05 09:35:58 boot_unlock 成功,09:35:59 即被 admin.php ip_block,09:42 只能靠宝塔手工改 config.php 脱困。
This commit is contained in:
@@ -4,9 +4,14 @@
|
||||
// 不带/带错令牌一律返回 nginx 原生 404 页(与文件不存在时完全一致)。
|
||||
// 注意必须放在 secGatePage() 之前 —— 令牌层先判(两层现已统一返回 nginx 404,
|
||||
// 但顺序仍要保持:引导模式下令牌层放行、页面层负责跳配置页)。
|
||||
// ★ boot 恢复模式(2026-10-05):令牌层也认 boot 令牌 —— 否则被锁在外面的运维
|
||||
// 敲 admin.php?boot=<引导令牌> 会在令牌层就吃 404,连"送回引导配置页"的
|
||||
// 恢复路由(secGatePage 里)都到不了。放行令牌层后,下面的 secGatePage()
|
||||
// 会把非白名单的 boot 持有者 302 到 admin_2fa_setup.php 去把自己加白,
|
||||
// 页面本身对非白名单 IP 依旧不渲染(白名单校验没有被绕过)。
|
||||
// 第二层 IP 白名单 —— 页面本身也只对白名单 IP 开放。
|
||||
require_once __DIR__ . '/security.php';
|
||||
secTokenGate();
|
||||
secTokenGate(true);
|
||||
secGatePage();
|
||||
// 令牌在 URL 上,禁止页面内任何外链通过 Referer 把它带走
|
||||
header('Referrer-Policy: no-referrer');
|
||||
|
||||
@@ -9,6 +9,15 @@ secEnsureSession();
|
||||
// IP 白名单闸门;boot 引导会话(配置流程中转)也放行
|
||||
secGatePage(true);
|
||||
|
||||
// ★ boot 恢复模式(2026-10-05):持有 boot 凭据但 IP 不在白名单 → 直接送回引导配置页。
|
||||
// 不加这一跳的话,下面的链路对非白名单 IP 全是 404 死胡同:
|
||||
// 未登录 → 跳 admin_login.php → 404;已登录已过2FA → 跳 admin.php?token= → 404
|
||||
// (线上 09:35:58 boot_unlock 后 1 秒就吃了 admin.php 的 ip_block,实录见 db/security.log)。
|
||||
// 白名单校验本身没有被绕过 —— 本页对非白名单 IP 依旧不渲染,只是不再把人丢去 404。
|
||||
if (!secIpAllowed() && secBootAuthenticate()) {
|
||||
secBootRecoveryRedirect();
|
||||
}
|
||||
|
||||
$enabled = defined('SEC_2FA_ENABLED') && SEC_2FA_ENABLED;
|
||||
$err = '';
|
||||
|
||||
|
||||
+25
-1
@@ -271,6 +271,12 @@ if (file_exists($secLogPath)) {
|
||||
$all = @file($secLogPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if (is_array($all)) $logLines = array_slice($all, -8);
|
||||
}
|
||||
|
||||
// ===== boot 恢复视角下"我的 IP 是否已被白名单接受" =====
|
||||
// secIpAllowed() 读的是请求开始时定义的 ADMIN_IP_WHITELIST 常量 —— 本页刚点过
|
||||
// 「保存白名单」时它还是旧值(secWriteConfig 只改磁盘)。不补判一次的话:刚把 IP
|
||||
// 加进去、页面上的恢复提示横幅却还在说"你不在白名单",运维会以为根本没保存成功。
|
||||
$wlAllowsMe = secIpAllowed() || secIpInWhitelist($myIp, $whitelist);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
@@ -333,7 +339,11 @@ if (file_exists($secLogPath)) {
|
||||
<div>
|
||||
<?php if ($mode === 'boot'): ?><span class="badge b-boot">引导模式(boot 令牌)</span><?php endif; ?>
|
||||
<?php if ($mode === 'init'): ?><span class="badge b-boot">首次初始化</span><?php endif; ?>
|
||||
<?php if (!$needInit): ?><a href="<?php echo $twoFaOn ? 'admin_2fa.php' : htmlspecialchars(secTokenUrl('admin.php'), ENT_QUOTES, 'UTF-8'); ?>">← 返回</a><?php endif; ?>
|
||||
<?php
|
||||
// "← 返回"指向 admin_2fa.php / admin.php?token= —— 两者对非白名单 IP 都是 404。
|
||||
// boot 恢复模式下你的 IP 正是不在白名单才进来的,藏掉这个会把人直接弹回死胡同。
|
||||
if (!$needInit && ($mode !== 'boot' || $wlAllowsMe)):
|
||||
?><a href="<?php echo $twoFaOn ? 'admin_2fa.php' : htmlspecialchars(secTokenUrl('admin.php'), ENT_QUOTES, 'UTF-8'); ?>">← 返回</a><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub">第一层:TOTP 二次验证(6 位动态码) | 第二层:管理面 IP 白名单(未配置时仅本机可访问)</div>
|
||||
@@ -341,6 +351,16 @@ if (file_exists($secLogPath)) {
|
||||
<?php if ($err !== ''): ?><span class="msg err">❌ <?php echo $err; ?></span><?php endif; ?>
|
||||
<?php if ($ok !== ''): ?><span class="msg ok">✅ <?php echo $ok; ?></span><?php endif; ?>
|
||||
|
||||
<?php if ($mode === 'boot' && !$wlAllowsMe): ?>
|
||||
<!-- boot 恢复模式的唯一正事:把当前 IP 加进白名单。其它管理页对你一律 404,这是设计如此。 -->
|
||||
<span class="msg" style="background:rgba(210,153,34,0.10);border:1px solid rgba(210,153,34,0.40);color:#d29922">
|
||||
🛟 引导恢复模式:你的 IP <b style="font-family:monospace"><?php echo htmlspecialchars($myIp, ENT_QUOTES, 'UTF-8'); ?></b>
|
||||
不在 IP 白名单里,所以后台其它页面对你一律返回 404(最小化信息透露,属正常表现)。<br>
|
||||
请到下方「🚪 第二层 · IP 白名单」把它加进去并点 <b>保存白名单</b> ——
|
||||
保存成功后立刻就能正常打开后台,不需要再去宝塔改 config.php。
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($needInit): ?>
|
||||
<!-- ===== 首次初始化:管理密码还是占位符时,本页唯一的入口 ===== -->
|
||||
<form method="post" autocomplete="off">
|
||||
@@ -403,6 +423,10 @@ if (file_exists($secLogPath)) {
|
||||
<div class="hint">🔖 后台唯一入口(请收藏这一条,不带 token 访问 admin.php 一律返回 404):<br><code><?php echo htmlspecialchars($adminUrl, ENT_QUOTES, 'UTF-8'); ?></code>
|
||||
<button type="button" class="copy" onclick="copyText('<?php echo htmlspecialchars($adminUrl, ENT_QUOTES, 'UTF-8'); ?>', this)">复制地址</button>
|
||||
<button type="submit" class="btn btn-gray" style="margin-left:6px" onclick="return rotateConfirm()">重新生成令牌</button></div>
|
||||
<?php if ($mode === 'boot' && !$wlAllowsMe): ?>
|
||||
<div class="hint" style="color:#f85149">⚠️ 你的 IP 还没加进白名单,现在打开上面这个入口<b>会是 404</b> ——
|
||||
请先在下方「IP 白名单」里保存,再访问它。</div>
|
||||
<?php endif; ?>
|
||||
<div class="hint">恢复访问地址:<code><?php echo htmlspecialchars($recoverUrl, ENT_QUOTES, 'UTF-8'); ?></code>
|
||||
<button type="button" class="copy" onclick="copyText('<?php echo htmlspecialchars($recoverUrl, ENT_QUOTES, 'UTF-8'); ?>', this)">复制</button></div>
|
||||
</div>
|
||||
|
||||
+49
-1
@@ -150,6 +150,14 @@ function secBootSessionActive() {
|
||||
*/
|
||||
function secBootAuthenticate() {
|
||||
if (secBootToken() === '') return false;
|
||||
// ★ 先看"有没有凭据可验",再决定要不要开会话(2026-10-05)。
|
||||
// secGatePage / secTokenGate 现在会为所有被白名单挡住的请求调用本函数做恢复路由,
|
||||
// 若无条件 session_start(),扫目录的匿名流量会每次刷出一个 session 文件。
|
||||
// URL 没带 boot、浏览器也没带会话 cookie、当前也没有活动会话 → 无凭据可验,直接失败。
|
||||
$hasParam = isset($_GET['boot']) || isset($_POST['boot']);
|
||||
if (!$hasParam && session_status() !== PHP_SESSION_ACTIVE && empty($_COOKIE[session_name()])) {
|
||||
return false;
|
||||
}
|
||||
secEnsureSession();
|
||||
$t = '';
|
||||
if (isset($_GET['boot'])) $t = (string)$_GET['boot'];
|
||||
@@ -370,9 +378,43 @@ function secIsBootstrapped() {
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* 页面级闸门。$allowBoot=true 时接受 boot 令牌解锁(仅配置页使用)。
|
||||
* boot 恢复模式的落点(2026-10-05)。
|
||||
*
|
||||
* 背景(线上 db/security.log 实录):运维被锁在外面 → 拿 boot 令牌解锁成功
|
||||
* (boot_unlock)→ 紧接着去开 admin.php?token=... → 依旧 ip_block_page 404。
|
||||
* 也就是说旧逻辑只在 admin_2fa_setup.php 这一个点上豁免了白名单,从其它任何
|
||||
* 管理页进来的 boot 持有者都会撞 404 死胡同,**根本到不了那个唯一豁免页**,
|
||||
* 结果还是被锁在外面(当天 09:42 只能靠宝塔手工改 config.php 才脱困)。
|
||||
*
|
||||
* 现在:持有有效 boot 凭据(?boot= 令牌 或 已建立的 boot 会话)却仍不在白名单
|
||||
* → 不丢 404,一律 302 送回引导配置页(唯一能把自己 IP 加白的页面)。
|
||||
*
|
||||
* 信息透露边界不变:没 boot 凭据的试探者照旧拿到与"文件不存在"完全一致的
|
||||
* nginx 404,本函数根本不会被调用;能走到这里的人本就读得到 config.php
|
||||
* (= 已经握有 ADMIN_PASS / SECRET_KEY / 2FA 密钥),路由给他没有新增暴露面。
|
||||
*/
|
||||
function secBootRecoveryRedirect() {
|
||||
$path = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '-';
|
||||
secLog('boot_route', 'path=' . $path);
|
||||
// URL 上还带着 ?boot= 就原样带上:会话一旦失效(换网络/清 cookie)还能自愈,
|
||||
// 不必让运维再手抄一次令牌。
|
||||
$q = '';
|
||||
if (isset($_GET['boot'])) {
|
||||
$t = (string)$_GET['boot'];
|
||||
if ($t !== '' && hash_equals(secBootToken(), $t)) {
|
||||
$q = '?boot=' . rawurlencode($t);
|
||||
}
|
||||
}
|
||||
header('Location: admin_2fa_setup.php' . $q);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面级闸门。$allowBoot=true 时接受 boot 令牌解锁(引导配置页 / 2FA 页使用)。
|
||||
* - 已引导 且 不在 IP 白名单 → nginx 原生 404(不是 403:403 会告诉探测者
|
||||
* "这个文件确实存在",与最小化信息透露原则冲突);
|
||||
* - 【boot 恢复模式(2026-10-05)】不在白名单 但 持有效 boot 凭据 → 不返回 404,
|
||||
* 改 302 到引导配置页把自己加白(详见 secBootRecoveryRedirect 注释);
|
||||
* - 未引导(全新部署第一次访问)→ 放行;后台主入口 admin.php 不直接渲染,
|
||||
* 改跳到 admin_2fa_setup.php 完成白名单 / 令牌 / 2FA 的首次配置。
|
||||
*/
|
||||
@@ -394,6 +436,12 @@ function secGatePage($allowBoot = false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ boot 恢复模式:$allowBoot=false 的页面(admin.php / admin_login.php 等)
|
||||
// 不渲染,但持有 boot 凭据的人不能被丢去 404 —— 送到唯一豁免页去加白名单。
|
||||
if (secBootAuthenticate()) {
|
||||
secBootRecoveryRedirect();
|
||||
}
|
||||
|
||||
// 已引导且不在白名单 → nginx 原生 404(绝不回 403:403 等于承认文件存在)。
|
||||
// 维护者不需要提示页 —— 服务器在他手上,他知道该查 db/security.log;
|
||||
// 试探者只能看到与"文件不存在"完全一致的表现。恢复入口写在 config.php 注释里
|
||||
|
||||
Reference in New Issue
Block a user