欢迎来到 Xiuno BBS

Xiuno BBS 5 整站崩溃与后台不可达排查实战经验 By TRAE SOLO

本文档记录 2026-10-16 xiuno5 站点"大量请求重定向到主页、无法登录后台"故障的完整排查过程。表层是一个症状,底层是五条独立缺陷的连锁反应。排查方法和每条机制对后续 Xiuno BBS 开发/适配具有普适参考价值。


第一章:症状全景与因果链

1.1 用户可见症状

症状表现
页面顶部嵌入 PHP 报错<fieldset> 里出现 Error[2]: Trying to access array offset on null
Cookie 存不住登录后刷新即失效;响应头出现互相打架的 Set-Cookie
重定向失效依赖 header('Location') 的跳转全部落空,或经 JS 兜底跳到主页
后台进不去提交后台密码后回到前台主页
首次进站即登录必失败报 CSRF token mismatch

1.2 因果链(重要:不是并列的五个 bug,是一条链 + 两个独立缺陷)

站点重装(bbs_kv 清空)
  └→ Aether 主题设置丢失,setting_get() 返回 NULL
      └→ 模板直接 $setting['appearance']['color'][...] 取值
          └→ Error[2] 警告被错误处理器 echo 进页面(DEBUG=1)
              └→ 页面开始输出,headers 关闭
                  ├→ setcookie() 全部失败 → 会话/登录态无法维持
                  ├→ header('Location:') 全部失败 → 重定向异常
                  └→ 登录 POST 走到 CSRF 层时 token 通道已残 → 无法登录

独立缺陷 A:.htaccess 缺 admin 伪静态规则 → 后台 URL 落进前台路由 default 分支渲染主页(与上述链条无关,单独存在)。

独立缺陷 B:runtime/tmp 编译缓存粘滞 → 改了源码不生效,排查时极易误判(详见 3.5)。


第二章:排查方法论(三板斧)

2.1 看原始响应头,不要只看页面

curl -D - 直接看 Set-Cookie,浏览器 DevTools 会掩盖细节:

curl -s -D - -o NUL http://localhost/xiuno5/admin/

本次通过响应头发现关键证据——同一个 bbs_sid 被发了又删:

Set-Cookie: bbs_sid=4eae87bd...; Max-Age=8640000
Set-Cookie: bbs_sid=deleted; Max-Age=0

2.2 找"最早的 output started at",而不是最后一条报错

页面里往往有一串 Cannot modify header information - headers already sent by (output started at ...)。output started at 指向的位置才是真凶(第一个产生输出的地方),后面的报错全是它的下游。本次链条:

  1. 第一条报错:output started at xiunophp.min.php:56 → 追查发现那是错误处理器自己的 echo,说明有一个更早的警告被打印
  2. 翻到页面 HTML 最顶部 → 找到最早的原始警告:view_htm_user_login.htm:125 的 null 取值

规则:从页面 HTML 的第 0 字节开始找第一个 Error 块,顺藤摸瓜。

2.3 用 curl 复现请求链,隔离前端变量

浏览器会话混入大量状态(cookie、JS),curl 能干净地验证服务端行为:

# 逐跳追踪重定向
curl -s -D - -o NUL <url>
# 完整跟随
curl -s -o NUL -w "%{http_code} -> %{redirect_url}\n" <url>

第三章:五条缺陷的机制与修复

3.1 设置丢失 + 模板 null 直取(根因)

机制:主题/插件设置存在 bbs_kv 表 k='setting' 行(xn_json_encode 存储)。站点重装清库后该行消失,setting_get() 返回 NULL。Aether 模板大量直接 $setting['panel']['section']['option'] 取值,PHP 8 下对 null 取偏移 = Error[2] 警告。

修复(两层防御):

  1. 数据恢复:从旧库 xiuno4.bbs_kv 的 setting 行提取 abs_theme_aether_setting 子树,REPLACE 回 xiuno5.bbs_kv,再 DELETE FROM bbs_cache WHERE k LIKE '%setting%' 强制回源(缓存键为 bbs_setting,= cachepre bbs_ + key)
  2. 自愈兜底(一劳永逸):在 plugin/abs_theme_aether/hook/index_inc_route_before.php(所有请求必经)检测设置缺失时,从 conf.php 的 panels 默认值重建并 setting_set() 持久化,等价 install.php 的金桔初始化

教训:主题模板永远不要裸取设置值;至少在设置加载点做一次存在性兜底,而不是在 60+ 处使用点逐个加 ??。

3.2 csrf_token() 的 setcookie 无守卫

机制:CSRF 双通道设计(session + cookie xn_csrf),但 csrf_token() 无条件 setcookie()。主题在页面中部(HTML 已输出后才调用)调 csrf_field() → setcookie 必然失败 → DEBUG 下又打印报错,进一步污染页面。

修复:model/misc.func.php 中加守卫:

if(!headers_sent()) setcookie('xn_csrf', ...);

3.3 首访 session 不落库 → CSRF token 必须提前下发(隐蔽坑)

机制:Xiuno 的 sess_new()(model/session.func.php)有 cookie_test 机制:第一次请求只发 cookie_test 探针,不创建 session 行。第二次请求校验通过才落库。这意味着首访请求里写进 $_SESSION 的任何数据(包括 csrf_token)在关机时无处可写,直接丢失。

后果:用户第一次进站就登录(很常见!),POST 时 session 里没有 token、cookie 也没有(见 3.2)→ csrf_token() 现生成新随机值 → 必然 mismatch。

修复:index.inc.php 在 check_runlevel() 之后、路由之前显式调一次 csrf_token()——此刻 headers 尚未关闭,cookie 通道能成功下发,token 稳定性不再依赖 session 落库:

check_runlevel();
csrf_token(); // 提前下发 xn_csrf cookie:首访 session 尚未落库,token 需走 cookie 通道

教训:任何"写 session 并期望下次请求读到"的逻辑,都必须考虑首访 session 不落库这一前提。

3.4 .htaccess 缺 admin 伪静态规则(后台不可达的真凶)

机制:后台入口是独立的 admin/index.php(含 SKIP_ROUTE),但后台内部的伪静态 URL(如 admin/index-login.htm)不是真实文件。通用兜底规则把它重写给前台 index.php,前台 switch 没有 admin 分支 → default → 渲染主页。表现为"提交后台密码后回到主页"。

修复:admin 规则必须放在兜底规则之前(rewrite 是先到先得):

# 后台伪静态:admin/ 下的路由交给后台入口(必须在前台兜底规则之前)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^admin/(.*)$ admin/index.php?$1 [QSA,L]

# 前台兜底(原有)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?$1 [QSA,L]

注意:admin 路由器同样用 param(0) 解析查询串(index-login.htm → route=index、action=login),所以捕获组 $1 要去掉 admin/ 前缀传入。!-f 保证后台静态资源(css/js/图片)仍直出。后台 AJAX 靠 header.inc.htm 里的 window.xn_csrf_token 走 X-CSRF-Token 请求头,与前台同一套 csrf_check。

3.5 runtime/tmp 编译缓存粘滞(排查期最大陷阱)

机制:_include()(model/plugin.func.php)的重编译条件是 !is_file($tmpfile) || DEBUG > 1——DEBUG=1 时不重编译,源码改动不生效。本次修改 misc.func.php 后实测仍是旧行为,一度误导诊断方向。

修复:改源码后手动清空 runtime/tmp/(整个目录是编译缓存,安全删除,会自动重建)。

教训:在 Xiuno 上"改了代码没生效",第一反应应该是清 runtime/tmp,而不是怀疑改错地方。


第四章:Xiuno 关键机制备忘(本次实证)

机制结论
kv 存储bbs_kv 表,k 超过 32 字符自动 md5;值为 xn_json_encode(标准 JSON);kv_cache_get 先查 cache(键 bbs_+key)再回源 DB
设置读取setting_get($k) 返回 array_value($g_setting, $k, NULL)——缺失即 NULL,调用方必须兜底
runlevelconf.php 的 runlevel=5 是正常开放值;0-4 各有限制,5 不触发任何 message
session自定义 handler 落 bbs_session/bbs_session_data 两表;首访只发 cookie_test 不落库;session.name 为 bbs_sid
编译_include() 把源码合并 hook 后写入 runtime/tmp/,<template include> 展开 + plugin_compile_srcfile 两趟处理
后台入口admin/index.php → define SKIP_ROUTE → include 根 index.php → 走 admin/index.inc.php 自己的 switch;DEBUG<3 时要求 gid==1 + admin_token_check()
错误显示DEBUG=1 时错误处理器把警告 echo 进页面正文——既是排查利器,也是 headers 污染源

第五章:给 AI Agent 的规则清单

  1. Xiuno 排查三板斧:curl 看原始响应头 → 页面 HTML 顶部找最早 Error 块(output started at 是关键线索)→ curl 复现请求链
  2. 改源码后必须清 runtime/tmp/,否则测的是旧代码
  3. 主题/插件模板禁止裸取设置值;在设置加载点做一次兜底(缺失时从默认值重建并持久化)
  4. setcookie/header 前判断 headers_sent(),尤其在页面中部调用的公共函数里
  5. 依赖 session 跨请求传值的逻辑,必须考虑首访不落库(cookie_test 机制)
  6. 新增伪静态路径前缀(如 admin)时,必须同时在前台兜底规则之前加专用规则
  7. 恢复设置数据:旧库 bbs_kv.setting 行 → 新库 REPLACE + 清 bbs_cache 对应键
  8. CSRF 链路验证方法:GET 拿表单 token + cookie → 立即 POST 假凭据 → 看返回的是 CSRF mismatch(链路断)还是业务报错(链路通)
0 0 3
复制成功

回复 (3)

Tillreetree 8天前
#2
哦对,忘了说,xiuno 5附带的伪静态规则可能对子目录无效,且还得手动给后台admin目录再打个补丁才能正常使用
leo 5天前
#3
Tillreetree 哦对,忘了说,xiuno 5附带的伪静态规则可能对子目录无效,且还得手动给后台admin目录再打个补丁才能正常使用
感谢反馈,会尽快处理更新。
leo 1天前
#4
Tillreetree 哦对,忘了说,xiuno 5附带的伪静态规则可能对子目录无效,且还得手动给后台admin目录再打个补丁才能正常使用
已经更新,有问题可以发帖反馈。