diff --git a/config.php b/config.php
index 11c3d5d..4957b25 100644
--- a/config.php
+++ b/config.php
@@ -116,5 +116,10 @@ define('CAPTCHA_PROVIDER', 'CF'); // ← 只改这个值:'CF' 用 Cloudflare
// 被锁在外面时的恢复方法:宝塔/SSH 查看本文件补全后的 SEC_BOOT_TOKEN,访问
// https://你的域名/plugin/admin_2fa_setup.php?boot=<令牌>
// 进入配置页把自己当前 IP 加入白名单(boot 令牌本身也是配置页入口)。
+// ★ 令牌全丢的最后手段(2026-10-09):宝塔/SSH 手工给下面 SEC-UPDATE 区的
+// ADMIN_IP_WHITELIST 数组加一条字面量 '0.0.0.0' —— 应急模式立即生效:
+// 除服务器本机外全站所有页面/API 一律 302 锁回 admin_2fa_setup.php(免密进入),
+// 按该页红色横幅指引删掉 0.0.0.0 + 「重新生成令牌」即恢复常态。
+// ⚠️ 0.0.0.0 只是应急门票(等价全网可进引导页),用完必须立刻删除。
// >>>SEC-UPDATE-BEGIN 本区段由 admin_2fa_setup.php 自动维护,请勿手工改动标记行
// >>>SEC-UPDATE-END
diff --git a/install.php b/install.php
index 7e0170e..df2d724 100644
--- a/install.php
+++ b/install.php
@@ -23,8 +23,9 @@ require_once __DIR__ . '/security.php';
// ① 已装过 → 与文件不存在完全一致的表现(本页也可能还没来得及自删)
if (is_file(__DIR__ . '/install.lock')) secNginx404();
-// ② IP 白名单 / boot 引导令牌 / 未引导放行
-secGatePage(true);
+// ② 不做 IP 白名单验证(2026-10-09 需求):全新站根本没有白名单可言,拿白名单
+// 挡安装页只会把部署者锁在门外;已引导站点由下面 ③ 的令牌层把关(?token= / ?boot=)。
+// 原 secGatePage(true) 在「已引导但没装库」时会把非白名单 IP 挡成 404。
// ③ 动态访问令牌(config 里有 SEC_ACCESS_TOKEN 时才生效;全新部署为空 → 放行)
secTokenGate(true);
secEnsureSession();
@@ -35,6 +36,7 @@ define('INSTALL_MD', defined('SQL_MD_PATH') ? SQL_MD_PATH : __DIR__ . '/sql.md
$cur = sqlMdDbConfig(); // static 缓存,此刻读的是「写入前」的旧配置,仅用于回显
$err = '';
$ok = '';
+$saved = null; // 本次 POST 成功写入的配置 → 完成页回显供截图/手抄
// CSRF:会话级随机数(不依赖 SEC_ACCESS_TOKEN,全新部署时它还是空的)
if (empty($_SESSION['inst_csrf'])) {
@@ -383,14 +385,19 @@ $val = function ($k, $d = '') use ($cur, $err) {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $err !== '' && isset($_POST[$k])) {
return htmlspecialchars((string)$_POST[$k], ENT_QUOTES, 'UTF-8');
}
- if ($k === 'dir') return htmlspecialchars($cur['dir'], ENT_QUOTES, 'UTF-8');
+ $cur = is_array($cur) ? $cur : array();
+ if ($k === 'dir') return htmlspecialchars(isset($cur['dir']) ? (string)$cur['dir'] : '', ENT_QUOTES, 'UTF-8');
if ($k === 'mode') return '';
if (preg_match('/^(java|web)_(host|port|name|user|pass)$/', $k, $m)) {
- $e = $cur[$m[1]];
+ // ★ 全新站 sql.md 配置区账密全空 → sqlMdDbConfig 的 java/web 槽是 null
+ // ("字段不全 → null → 该侧兜底 db"),直接 $e[...] 在 PHP 8 会每个输入框
+ // 报一条 "Trying to access array offset on null"(实录:test12 第 391 行)。
+ $e = (isset($cur[$m[1]]) && is_array($cur[$m[1]])) ? $cur[$m[1]] : array();
$map = array('host' => 'host', 'port' => 'port', 'name' => 'name', 'user' => 'user', 'pass' => 'pass');
- return htmlspecialchars((string)$e[$map[$m[2]]], ENT_QUOTES, 'UTF-8');
+ $rv = isset($e[$map[$m[2]]]) ? (string)$e[$map[$m[2]]] : '';
+ return htmlspecialchars($rv, ENT_QUOTES, 'UTF-8');
}
- return htmlspecialchars($d, ENT_QUOTES, 'UTF-8');
+ return htmlspecialchars((string)$d, ENT_QUOTES, 'UTF-8');
};
?>
@@ -447,12 +454,40 @@ ul{margin:6px 0;padding-left:20px}
- 配置已写入
sql.md 并回读校验通过。
- 已生成
install.lock,本页再次访问将返回 404。
- install.php 已自我删除。
+ install.php 已自我删除(后台静默完成,页面照常停留供你截图留档)。
+
+
+
+
📋 本次写入的配置 —— 请截图或手抄留档:
+
+
+ | 运行模式 |
+ |
+
+
+
+ | 存储位置 |
+ |
+
+
+ 'Java 侧(sync.php · Java.db)', 'web' => 'PHP 侧(web.db)') as $slot => $label): ?>
+
+
+ |
+ (空 → 自动回退 db 文件) |
+
+
+
+
+
本页已随安装完成而删除 —— 这是最后一处能整份查看该配置的页面;完整配置仍保存在 sql.md。
+
+
+
下一步:
- - db 模式 → 直接访问 后台登录页。
- - mysql 模式 → 后台首访会自动建表(已在本页实连并验证过建表权限)。
+ - 先去 安全配置引导页:把你当前的 IP 加入白名单、保存访问令牌、设置 2FA。
+ - mysql 模式 → 配完安全项首次进后台会自动建表(已在本页实连并验证过建表权限);db 模式 → 配完即可登录。
- 记得让 Java 端(api/sync.php 所在的独立站点)也用同一份配置:改 sync.php 时两处都要传。
diff --git a/security.php b/security.php
index cb86afe..9fc93d0 100644
--- a/security.php
+++ b/security.php
@@ -273,6 +273,25 @@ function secCidrMatch($ip, $cidr) {
* 规则:127.0.0.1/::1 永远放行(服务器本机逃生舱);
* 白名单为空 → 除本机外任何人一律拒绝(含 IP 缺失的异常请求)。
*/
+/**
+ * 0.0.0.0 应急模式(2026-10-09):白名单里出现字面条目 `0.0.0.0`。
+ *
+ * 场景:boot 引导令牌与后台访问令牌全丢、又没有别的入口 —— 只要能上服务器
+ * (宝塔/SSH)把 config.php 的 ADMIN_IP_WHITELIST 加上一条 0.0.0.0,即可临时恢复:
+ * - 全站其它页面/API 一律被 security.php 尾部的全局应急路由 302 回
+ * admin_2fa_setup.php(应急条目只买得到这一张门票,用来删掉它自己 + 重置令牌);
+ * - admin_2fa_setup.php 以 boot 引导模式免密放行(见该页 $isEmergency 分支);
+ * - 服务器本机 127.0.0.1 / ::1 不受应急路由限制(宝塔/SSH 永远有救)。
+ * 用完必须立刻删掉 0.0.0.0 —— 它等价于"全网都能进引导页"。
+ */
+function secIsEmergency() {
+ if (!defined('ADMIN_IP_WHITELIST') || !is_array(ADMIN_IP_WHITELIST)) return false;
+ foreach (ADMIN_IP_WHITELIST as $e) {
+ if (trim((string)$e) === '0.0.0.0') return true;
+ }
+ return false;
+}
+
function secIpAllowed() {
$ip = secClientIp();
if ($ip === '') {
@@ -284,6 +303,9 @@ function secIpAllowed() {
foreach (ADMIN_IP_WHITELIST as $entry) {
$entry = trim((string)$entry);
if ($entry === '') continue;
+ // ★ 0.0.0.0 = 应急全放行条目:IP 层直接开门,但非引导页根本到不了门禁 ——
+ // security.php 尾部的全局应急路由会在 require 阶段就把它们 302 回引导页。
+ if ($entry === '0.0.0.0') return true;
if ($entry === $ip) return true;
if (strpos($entry, '/') !== false && secCidrMatch($ip, $entry)) return true;
}
@@ -617,6 +639,9 @@ function secGatePage($allowBoot = false) {
// → 先放行,让部署者完成首次配置;后台主入口改跳配置页,这就是"引导"的落点。
if (!secIsBootstrapped()) {
secLog('ip_gate_open_bootstrap', 'path=' . (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '-'));
+ // ★ 应急模式下直接放行:全局钩子只放 admin_2fa_setup.php 走到这里,
+ // 不能再把它转送去 install(那是常规首跑的顺序,应急时以自救为先)。
+ if (secIsEmergency()) return;
$script = basename(isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '');
// ★ 首跑顺序(2026-10-09):装库先于安全配置 —— install.php 还没跑完
// (文件在场、无 install.lock)时,任何引导入口一律先 302 到自毁式安装页;
@@ -1457,6 +1482,27 @@ function secRateTrusted() {
return false;
}
+/**
+ * 全局应急路由(2026-10-09):include security.php 时自动执行一次(在限流层之前)。
+ *
+ * 白名单含 0.0.0.0 时,除 admin_2fa_setup.php 与服务器本机外,任何加载本文件的
+ * 入口(admin 系 + core 系全部页面/API)一律 302 送回应急引导页 —— 0.0.0.0 只放行
+ * 到那一页,进去删掉应急条目 + 重置令牌即恢复常态。
+ * 用 302 不用 301:301 会被浏览器永久缓存,改回白名单后客户端仍会继续乱跳。
+ * 无 REMOTE_ADDR(CLI/内部调用)不参与。
+ */
+function secEmergencyGate() {
+ if (empty($_SERVER['REMOTE_ADDR'])) return;
+ if (!secIsEmergency()) return;
+ $script = basename(isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '');
+ if ($script === 'admin_2fa_setup.php') return; // 唯一放行页(引导用户自救)
+ if (secIsLocalIp(secClientIp())) return; // 本机恒通:宝塔/SSH 永远有救
+ secLog('emergency_route', 'path=' . (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '-'));
+ header('Location: admin_2fa_setup.php', true, 302);
+ exit;
+}
+secEmergencyGate();
+
/** 第四层入口:include security.php 时自动执行一次。 */
function secRateLimitGate() {
if (PHP_SAPI === 'cli') return; // 命令行脚本不参与限流