最近更新 XIUNOX 更新日志 2026-10-31 [复制链接]

管理员组

🔐 安全:附件 API 与私有存储

  • 附件 API GET 越权枚举修复:Bearer 身份及所有者/管理员授权,白名单返回安全元数据,禁止泄露真实存储路径与 URL;下载响应 private/no-store。补充 Apache 压缩包及临时元数据拒绝规则、Nginx/CDN 上线处置说明和回归脚本。

  • 私有附件默认位置调整:改为站点目录内 upload/private/(0700,自动创建)。原默认 dirname(APP_PATH)/.xiunox-private-* 在 Web 根之外,被线上 open_basedir 拦死导致 protect() throw、下载页 500。root() 仅拒绝「私有目录 = Web 根本身」,子目录放行;强保护仍可用 conf/private_storage.php/XIUNOX_PRIVATE_PATH 指到 Web 根外。代价为弱保护:upload/.htaccess 加 RewriteRule 禁止 /upload/private/ 直链,Nginx 需加 location ^~ /upload/private/ { deny all; }。

  • PrivateAttachmentStorage::isPrivate():读路径捕获存储不可用异常降级 false(私有文件公开存储无副本,最坏 404 不泄露;写路径仍 throw 防静默落公开存储);root() 异常消息补充 open_basedir/XIUNOX_PRIVATE_PATH 排查指引。

  • 迁移工具 CLI 启动修复:tool/migrate_private_storage.php 原因 xiunophp 不加载 conf/conf.php 导致 $conf 为空、$db 为 NULL 无法启动;现自行 include conf.php、转绝对路径、加载 version.php,补 EscapeService/model/user.func.php,lib 类改 class_exists/include_once(修 HiddenContentGuard 重复声明 fatal)。本地 dry-run 通过(扫描 48 / 需保护 14)。

  • 在线升级链路加固:OnlineUpgradeService 开启 TLS 证书校验,解压前逐条目检查 ../、绝对路径、盘符穿越。

  • 4 处高危存储型 XSS 封堵:API 发帖/编辑标题补 strip_tags;API 签名 JSON body 补 xn_signature_purify + 长度校验;API 昵称 JSON body 补 htmlspecialchars;appcenter description/self_check_report 入库前 xn_html_purify。json_ld 输出加 JSON_HEX_* + strip_tags 封 </script> 逃逸;search 页标题先 esc_html 再打 mark;置顶列表与 moments 卡片补 esc_html。

  • xnx_hidden 五处泄露出口封堵:搜索摘要、帖子页 SEO description 与 og:image、xnx_theme_moments 列表摘要与卡片首图、xnx_fcomment 侧栏共享缓存改存 pid 按查看者渲染、xnx_ai_reply 送第三方 LLM 前过滤(新增 lib/HiddenContentGuard + 回归脚本)。

  • API 越权四处修复:thread/hot 补版块读权限;PUT /user/{uid} 改邮箱/改密须验当前密码并补邮箱唯一性;mod move(API+Web)补目标版块管辖校验;like/favorite/report 对不可读帖统一 404。

  • 框架层加固:db_orderby_to_sqladd 键名与 maxid 字段名加标识符白名单(双拷贝同步);post 回收站 LIKE 查询迁 db_sql_find_prepared;my_notify 链接 esc_attr;AttachmentService::saveToSession 同步 max(array_keys)+1 修复并发 key 覆盖。

  • 其他加固:待审头像文件名加随机段(防时间戳可枚举直链);xnx_setting_transfer 导入暂存 JSON 迁私有存储;审核队列回帖摘要过 ApiContentService::hidden。

  • 第二轮对抗性复核补修:API PUT /thread 标题补长度≤80 + 敏感词检查 + 被驳回帖编辑后重进待审;API 改签名移除并改指 PUT /my/signature;nickname 审核分支补 notify_new_pending + 计次;admin.php 日志筛选命名占位符改 ? 位置绑定;base64 头像上传 $destfile 未定义修复;appcenter 后台导入 description/version_changelog 入库前 xn_html_purify;pwa.js CSRF cookie 名修正。


⚡ 修复/性能:LIKE 通配符转义

  • 新增统一 helper xn_like_escape()(转义 \ _ %,\ 排首位;db.func.php 与 xiunophp.min.php 双拷贝同步),修复 8 处裸 LIKE 拼动态值:route/user.php @用户搜索、admin/route/log.php 日志用户检索、admin/route/thread.php 后台帖子搜索+回收站筛选、model/thread.func.php thread_find_by_keyword、api/v1/search.php 三类搜索、api/v1/thread.php 帖子列表 keyword、model/attach.func.php 附件文件名检索。

  • MySQL 缓存驱动 deleteByPrefix LIKE 未转义:前缀 _ 被当单字符通配符致几乎全表扫描(每次发帖/删帖清版块缓存都卡顿),且同长度异形键可能被误删。改 str_replace 一次性转义(\ 必须在首位),双拷贝同步。


📊 计数修复与统计口径

  • 审核通过链路守卫:AuditService 单条/批量 approve 的 thread 分支跳过 is_deleted;post 分支跳过 isfirst 与 is_deleted 并校验父主题存在且未软删;批量通过的作者通知从「全部入参」收紧为「实际通过项」。

  • 主题通过时同步首帖 post.audit_status(单条+批量):此前只更新 thread 表,导致已通过主题的楼主正文对普通用户隐藏。

  • post_restore 拒绝父主题已软删/不存在的回帖;回收站「评论」列表收敛为「活主题下软删回帖 + 孤儿回帖」。

  • 分页总数与列表同口径:首页「关注/精华」、用户主页/用户中心主题分页补 is_deleted=0;「我的回复」「我的主题」分页总数改实时 COUNT。

  • thread.likes 永久虚高修复:新增 post_like_delete_by_pids/post_like_delete_by_tid,接入 5 条硬删路径;软删/恢复保留点赞行保持对称。

  • user_purge 补清理被删用户发出的点赞/收藏行,逐帖递减。

  • 精华计数软删对称:thread_soft_delete/restore 补递减/回补 user.digests/forum.digests/runtime.digests,硬删加 is_deleted 守卫防二次扣减。

  • runtime_init 补初始化 digests,负数自愈条件补 digests。

  • 移动精华帖补 thread_digest_update 同步 fid 快照。

  • 批量幂等过滤:thread_soft_delete_batch/thread_restore_batch 补入库状态过滤(soften_tids/restore_tids),返回值改为实际处理数。

  • attach_delete 补删后按 pid 重算 images/videos/files。

  • post_delete_by_uid 补维护 thread.posts/lastpid,按 tid 汇总递减 + lastpid 重算 + 缓存失效。

  • 后台「重新计数」扩展:新增 thread.posts 全量重建;补 thread.likes 对账 post_like、thread.favorites 对账 thread_favorite、user.digests/forum.digests/runtime.digests 对账可见精华主题数、post.likes 每楼点赞对账;失效活跃用户榜缓存。


🚀 删帖性能与索引

  • post_delete_by_tid 重写:db_find 分批(500/批)+ 列裁剪避开 longtext,附件分批 unlink + 一次 IN DELETE,回帖本体走 KEY(tid,pid) 一次批量 DELETE,一并物理清除历史遗留软删回帖附件(记 thread_delete_perf 日志)。

  • post_delete_by_tids_batch 重写:tid 按 100/批 IN 查询 + 列裁剪;附件 pid 分 500/批走 IN DELETE。

  • post_delete_by_uid 重写:分批扫描 + 列裁剪,补齐此前从未清理的用户回帖附件。

  • thread_delete/thread_delete_batch 补 thread_top/thread_digest 硬删清理 + 按 uid/fid 聚合递减 digests 统计。

  • AttachAccessService::deletePermissionByTid 批量化(500 附件 = 500 SQL → 一次 IN DELETE)。

  • bbs_attach 新增 KEY tid,UpgradeService::upgradePerfIndexes() 幂等迁移 idx_tid(探测两个候选索引名避免重复)。

  • admin/route/thread.php op=delete/hard_delete 循环改走 thread_delete_batch(批删 SQL 从 8×N 降到 6 常数次)。


🔌 插件级联

  • 为 10 个插件补齐 model_thread_delete_batch_start/end hook:xnx_attach_access(_start)、xnx_hidden / xnx_poll / xnx_related / xnx_private_reply / xnx_comment_visibility / xnx_reward / xnx_fields / xnx_lottery / xnx_tag(_end)。修复后台批删/回收站硬删仅调用 thread_delete_batch 但无插件订阅该 hook,导致投票/悬赏/抽奖/话题/自定义字段等业务表数据永久残留。

  • xnx_reward 批末一次性 CacheHelper::pluginDeletePrefix,避免每 tid 触发 Redis SCAN;xnx_comment_visibility 批末单次 clearCache。


🔧 编译器与 hook

  • hook 编译兜底守卫漏洞修复:旧 strpos('// hook') 守卫与编译正则不等价,//HOOK x、//hook x、// hook x 等变体逃逸改写仍被二次展开,致 hook 内容线性拼接、同作用域 static 重复声明 fatal(实案线上 view_htm_post_list.inc.htm 帖子页 500)。改为无条件 preg_replace,任何可匹配形态一律改写为 // hook:NAME。

  • .htm hook 归因注释按占位符上下文区分:PHP 上下文(// hook xxx.htm 源标记)用裸行注释 + 内容剥离 PHP 标签,HTML 上下文(<!--{hook xxx}--> 源标记)保持 PHP 标签包装,防嵌套开标签 ParseError。实案修复 xnx_tag 话题标签致发帖页 post.htm:48 全站 ParseError(thread-create 白屏)。

  • xnx_reward v1.1.8:修复帖子页 500 Duplicate declaration of static variable $xnx_reward_post_data——post_list_inc_userinfo_actions_after.htm 与 thread_list_inc_subject_after.htm 改用 isset 守卫的普通变量替代 static 缓存(宿主模板同一 hook 占位符出现两份时编译产物会拼接两份 hook,PHP 8 编译期即 fatal);同作用域普通变量在 foreach 迭代间天然保持值,语义等价,重复拼接时第二份直接跳过。


🔍 线上排查记录

  • open_basedir 未含私有目录致 500:open_basedir(/www/wwwroot/xiunox.org/:/tmp/) 未含 /www/wwwroot/.xiunox-private-*,致 attach_format→isPrivate→root() throw,含附件帖子页 500(服务器侧扩充 open_basedir 解决)。

  • xnx_reward static 重复声明 fatal 复发观察:20:10/21:58 同 view_htm_post_list.inc.htm:288,ErrorHandler 对 tmp fatal 无条件 unlink → 复发 = 每次重建仍烘出重复 → 服务器文件集与本地不一致;本地编译试验台实测当前组合产物 static 恰 1 处 php -l 过,待服务器四条判别命令定位差异源。


🧩 插件版本

插件

版本

主要变更

xnx_feeds

1.4.4

点赞动态跳转 404 修复(按评论维度 extra.tid + 楼层锚点);楼层锚点格式 #pid-{pid} → #pid{pid}

xnx_fcomment

1.0.2

楼层锚点格式对齐全站约定

xnx_reward

1.1.8

static 缓存改 isset 守卫普通变量,修复帖子页 500

xnx_tag

1.1.4

后台设置页分页点击无效修复(URL 补 {page} 占位符 + 附带 keyword)

xnx_poll

1.1.2

后台投票管理页 404 修复(段式路由 + {page} 占位符 + $action 取 param(1))

xnx_demo_data

1.0.1

cleanupDemoData 同步维护计数(user.posts/thread.posts/runtime)

oulin_hongbao

1.0.2

三个 PHP hook 文件头注释改规范 // hook: xxx 冒号格式

xnx_appcenter

1.15.40

—

xnx_hidden

1.1.8

—

xnx_pwa

1.0.4

—

navgcn_ticket

1.2.4

—

xnx_setting_transfer

1.0.1

—

xnx_theme_moments

1.0.2

—

xnx_fcomment

1.0.1 → 1.0.2

—

xnx_ai_reply

1.0.5

—

xnx_attach_access

1.2.4

—

xnx_related

1.0.2

—

xnx_private_reply

1.0.1

—

xnx_comment_visibility

1.0.2

—

xnx_fields

1.0.4

—

xnx_lottery

1.3.11

—


📚 文档与后台

  • 后台「设置→固定链接」Nginx 伪静态片段补充 location ^~ {base}/upload/private/ { deny all; }(宝塔伪静态 + 自建 server 各一处,子目录安装自动带 base_path);Caddy 片段补 @xiunox_private path + respond 403。

  • docs/install.md 3.1.1 伪静态规则、3.1.2/7.4 完整示例同步补充该 deny;3.2 Apache 说明 upload/.htaccess 已内置禁止直链。

  • online-runbook 与迁移工具说明同步更新;.gitignore 忽略 upload/private/*。


⚠️ 升级提醒

  1. 私有附件策略变更:默认位置改为 upload/private/,需在 Nginx 配置 location ^~ /upload/private/ { deny all; },否则受保护附件可被直接下载;已有站点建议运行 tool/migrate_private_storage.php 迁移存量。

  2. 上线后需清理 tmp/ 编译缓存并刷新 OPcache(涉及 route/model 为 _include 编译文件)。

  3. 后台需执行一次「重新计数」 修复存量计数漂移(本地验证:user.posts 漂移 11 人、user.threads 2 人、thread.posts 41 帖)。

  4. xnx_reward v1.1.8 修复帖子页 500,升级后确认 post_list_inc_userinfo_actions_after.htm 与 thread_list_inc_subject_after.htm 编译产物中 static 声明仅 1 处。

  5. 已知接受项:同秒多次待审头像 glob 歧义(罕见)、st_import 暂存无 TTL(私有存储 0700)。

最新回复
  • xiunox 一级用户组

    感谢这份详尽的日志——信息密度很高,几个点值得在下个版本沉淀成规范:

    1. 私有附件弱保护是最大风险面:upload/private/ 靠 Nginx location ^~ + Apache 的 .htaccess 双保险,但子目录安装/多 CDN 回源场景极易漏配。建议加一个后台自检接口,主动请求 {base}/upload/private/ 期望 403,未配置时在后台红条告警。
    2. hook 编译守卫改为无条件 preg_replace 是对的,但根本问题在「同一占位符在模板中出现两份 → 编译拼接」这条路径。建议编译器在 // hook:NAME 处注入唯一标记并去重,或对同作用域 static 直接报编译期警告,而不是让插件作者各自用 isset 绕。
    3. 升级顺序建议明确写进 runbook:先改 Nginx/Apache 规则 → 跑 migrate → 清 tmp/ + OPcache → 执行「重新计数」,否则顺序颠倒会出现短暂直链暴露。

    另外第 20:10/21:58 的 static 复发,建议直接

    2小时前

请先登录后再回复 登录