bbs1.org安全审计报告

23次阅读
没有评论

bbs1.org 代码安全审计报告

  • 审计对象:/bbs1org(单文件 PHP 论坛 v8.5.16)
  • 技术栈:原生 PHP 8.1+,无框架无依赖;多数据库(SQLite / MySQL / PostgreSQL);单核心 index.php(3222 行) + 插件机制(app/optional/Plugin.php、Cron.php、Setup.php)
  • 审计日期:2026-08-10
  • 审计范围:反序列化、命令执行/RCE、SQL 注入、未授权访问、XSS、SSRF、越权、私信/通知功能,及能获取系统/用户/应用权限或数据的路径
  • 总体结论:安全基线良好 —— 全部 SQL 参数化、全站 POST 强制 CSRF、认证 Cookie HMAC 签名、Markdown 渲染默认全转义、核心无命令执行/反序列化 sink。发现 2 中危 + 4 低危 属实问题,均为逻辑/权限类缺陷,无高危远程利用链。

一、已确认漏洞(按严重度排序)

1. 受限版块内容泄露(越权访问 → 信息泄露)— 中危

项目 内容
位置 index.php:2597 topic_index_data()(首页/搜索/个人主页共用)
调用点 home_page:2780、user_page:2586、全局搜索 ?q=(需登录):2716
触发条件 站点存在设置了 allow_view_groups(仅限部分用户组浏览)的版块
影响 匿名用户可看到受限版块主题的标题、作者、发帖/最后回复时间、回复数、所在版块、最后回复人;个人主页 tab=replies 还可泄露回帖正文前 180 字摘要

漏洞成因:topic_index_data() 组装列表 SQL 时只按 forum_id 过滤,完全忽略版块的 allow_view_groups 浏览权限。对比之下,直接访问版块页(forum_page:2795)和主题页(topic_page:2821)都正确执行了 forum_group_allowed($f,'allow_view_groups');移动端版块条(:2729)也正确过滤。唯独列表查询漏了。

利用路径(无需登录):

# 攻击面 A1:匿名首页泄露
GET /index.php?a=home            → 受限版块主题出现在首页列表

# 攻击面 A2:个人主页泄露回帖摘要
GET /index.php?a=user&username=内部成员&tab=replies   → 泄露受限版块回帖正文前180字

# 攻击面 A3:全局搜索(需登录,普通用户即可)
GET /index.php?a=search  (POST q=关键词)

修复建议:在 topic_index_data() 的 SQL 中加入与 forum_group_allowed 等价的 allow_view_groups 过滤(先取当前用户可见版块 ID 集再 WHERE forum_id IN (...),需跨库兼容),三处入口一处修复全部覆盖。


2. 后台管理权限越权修改用户组/版块(权限提升)— 中危

项目 内容
位置 index.php:3083 admin_edit_page()(仅 need_admin());save_group():2338、save_forum():2323(自身无任何权限检查)
触发条件 拥有 allow_admin(后台管理)权限的用户
影响 可编辑/新建用户组(为自己所在组授予 allow_manage/allow_admin),可修改任意版块的浏览/发帖/回帖权限组

漏洞成因:权限模型不一致。admin_edit_page() 只要求 allow_admin,而删除用户组/版块却要求 can_manage() && is_super_user()(can_admin_delete:2176 + del():2490)。

利用路径:

# 步骤1:打开编辑表单取 CSRF token
GET /index.php?a=admin&do=edit&type=group&id=<自己所在组ID>

# 步骤2:POST 给自己组授予内容管理权
POST /index.php?a=admin&do=edit
type=group&id=<组ID>&name=普通组&allow_manage=1&allow_admin=1&_csrf=<token>
→ 获得 can_manage():删帖/禁言/置顶/改版块/删除用户

# 步骤3:新建全权限管理组
POST /index.php?a=admin&do=edit
type=group&id=0&name=新管理组&allow_manage=1&allow_admin=1&_csrf=<token>

# 步骤4:任意改版块权限(把受限版块设为全员可见)
POST /index.php?a=admin&do=edit
type=forum&id=<FID>&name=...&allow_view_groups=&allow_post_groups=&allow_reply_groups=&_csrf=<token>

影响评估(客观):allow_admin 本身已能进后台安装/上传插件(插件机制按设计允许加载任意 PHP = 管理员 RCE by design),因此本漏洞边际影响有限,但它是明确的权限模型不一致,且让 allow_admin 间接获得本不该有的 can_manage 能力。

修复建议:admin_edit_page() 的 group/forum 分支改为 need_manage(),与删除逻辑对齐。


3. 私信/通知功能:发送无任何限流(通知轰炸 / 存储型 DoS)— 低-中危

项目 内容
位置 index.php:2280-2287 user_notify_page() POST 分支 → create_notification()
触发条件 任意已登录用户
影响 脚本化高频 POST 向任意用户(含管理员 uid=1)无限发送私信,刷爆其通知红点和列表;每条私信落库一条 app_notifications 行,长期形成存储膨胀

漏洞成因:私信是唯一没有任何频率控制的写入口。对比:登录失败限流(security.rate_allow/hit:2521/2529)、注册限流(:2198/2270)、发帖间隔(post_interval_seconds)都有控制,唯独私信没有。

利用要点:已登录用户可取得自己页面的 CSRF token,脚本化高频 POST;:2279 只拦截"发给自己",不拦截发给管理员。

修复建议:在 create_notification() 的 direct 分支或 user_notify_page 加与发帖同级的频率限制(security.rate_allow/last_post_at),并给每条 recipient_id 加单位时间上限。


4. 无鉴权的 OPcache 清理接口 — 低危(未授权访问 + 轻微 DoS)

项目 内容
位置 index.php:772 opcache_refresh_route()
触发条件 app/data/opcache-refresh.lock 锁文件存在时(CLI 升级 → PHP-FPM 流程短暂创建,Setup.php:670-674)
影响 任何人可访问该接口触发 opcache_reset()(强制全部脚本重新编译,轻微性能 DoS),锁文件名固定可预测

修复建议:校验锁文件内容中的一次性随机值,或仅接受内网/本地来源。


5. 客户端 IP 可伪造,绕过注册/登录限流 — 低危

项目 内容
位置 index.php:742 ip_addr()
触发条件 站点未通过可信反向代理清洗 HTTP_CLIENT_IP / HTTP_X_FORWARDED_FOR / HTTP_CF_CONNECTING_IP 等头
影响 绕过注册限流(save_user:2198/2270)→ 批量注册刷号;绕过登录失败限流(login_page:2520-2529)→ 无限暴破

修复建议:默认只信任 REMOTE_ADDR;确需反代时用可配置的受信代理地址白名单 + 取 X-Forwarded-For 最右端非受信地址。


6. 被禁言的用户仍可发私信 — 低危(权限一致性)

项目 内容
位置 index.php:2277 user_notify_page() 只调 need_login(),无 need_speak()/is_muted() 检查
对比 发帖/回帖有 can_speak() 守卫(:1367-1370);禁访用户被 need_site_access() 全局拦截(:1417),只有禁言用户漏网
影响 被禁言的用户仍能私信骚扰他人

修复建议:发送端加 if (is_muted()) err('禁止发言');。


二、私信/通知功能专项核查(已验证安全的部分)

检查项 结论 依据
越权读他人私信(IDOR) ✅ 无 通知列表仅在 $own_profile 时加入允许列表(:2700),$profile_tab 非法即重置为 topics(:2704);mark_notifications_read 仅本人页触发(:2744-2745)
发信人身份伪造 ✅ 无 发送者固定取 uid()(HMAC 签名 Cookie,:2285)
目标校验 ✅ 有 目标必须存在(:2278),禁止发给自己(:2279)
内容 XSS ✅ 无 私信正文走 markdown_html()(:1011);quote 参数经 notification_excerpt 截断 + h() 渲染(:2291),URL 走 http_build_query(RFC3986)(:1715)
CSRF ✅ 有 发送是 POST,全局 check() 校验 _csrf(:1431)
SQL 注入 ✅ 无 私信写入、通知列表、@提及查询全参数化(:917/:970/:951)

三、已排查、未发现问题的类别

类别 结论 依据
反序列化 ✅ 无 全库无 unserialize;serialize 仅用于内部缓存键。form_error Cookie 仅 base64+json
命令执行 / RCE ✅ 无 核心无 eval/assert/system/exec/popen/proc_open;include 仅用于配置文件和已校验插件
SQL 注入 ✅ 无 q()/one()/val()/tx() 全参数化;标识符经 app_db_identifier 加引号;IN 列表全 int 强转;content_search_condition(:1920) 全文参数化;migrate 标识符经 migrate_identifier 加引号
XSS(含存储型) ✅ 无 markdown_html()(:1786) 先 h()(htmlspecialchars ENT_QUOTES) 全量转义再处理 @提及;摘要/简介/通知/搜索词/错误消息输出全部转义;flash 经 json_encode(JSON_HEX_TAG...)(:2072) 入 <script>;头像 style/seed 白名单+rawurlencode;highlight_style 正则限定 #hex 且 h() 包裹
SSRF ✅ 无(受控) remote_http_request()(curl 限制 HTTP/HTTPS)、us_http()(file_get_contents+3 次重定向) 只请求固定可信端点;migrate 仅管理员可用;无用户可控 URL 进入请求函数
未授权访问 ✅ 良好 后台/插件/市场/升级/迁移全部 need_admin();安装一次性锁;升级自有 us_need_admin()+CSRF 双校验;所有 POST 全局 CSRF
越权(其他) ✅ save_user() 非管理员只能改自己;发帖/回帖校验 allow_post_groups/allow_reply_groups;can_manage_topic/can_manage_reply 正确;del 对 users/groups/forums 要求 can_manage

四、存储型 XSS 专项核查(逐字段"写入 → 读取输出"链路)

字段 写入 输出 转义
主题标题 save_topic h($title) :1831 / h($t['title']) :2017 ✅
主题/回帖正文 app_topics.body / app_replies.body markdown_html($body) :1836 ✅
个人简介 bio save_user h($user['bio']) :1252 ✅
用户名 save_user 全局 h()(:1995/:2001/:1238/:1029) ✅
通知/私信内容 app_notifications.content markdown_html($body) :1011 ✅
通知引用 quote POST → 存储 h($quote) :2291 ✅
回帖摘要(个人主页) content_excerpt(存原文) h($reply_excerpt) :1994/:2012 ✅ 输出时转义
头像 style/seed save_user 白名单 + rawurlencode + h($src) :1688-1691 ✅
高亮色 highlight_style save_topic(需 can_manage) 正则限定 #[0-9a-fA-F]{6} + color: 前缀 :2389-2390;h() 包 style 属性 :2014 ✅
搜索词回显 $_GET['q'] h($q) :2064 ✅

关键机制:markdown_html() 处理顺序是 h($text) 整体转义 → content_special_links_html() 在已转义文本上做 @提及。@提及生成的 <a> 标签 href 经 h() 转义,用户名经 html_entity_decode 还原后走 route_url(rawurlencode),锚文本直接取自已转义文本。用户输入 <img onerror=...> 会被变成 &lt;img onerror=...&gt;,无法构造标签或属性逃逸。插件 Hook(markdown.after 等)可注入 HTML,但插件安装需 allow_admin = 管理员 RCE by design,不属存储 XSS 范畴。


五、用户信息暴露面(能否看到其他用户邮箱 / 其他用户信息)

邮箱 —— 看不到

  • 公开个人主页(a=user)只查询 id,username,bio,avatar_style,avatar_seed,group_id,points,is_banned,is_muted,不含 email(:2578)
  • 所有列表 attach_users()/attach_topic_list_users() 同样不带 email(:397/:405)
  • 私信页内存里 row() 取完整行含 email,但只输出用户名/组/头像,email 不进 HTML(:2278/:2292)
  • 邮箱仅在自己「个人资料」页可见(profile_page,need_login,:2567)
  • 核心后台无用户列表页,无查看他人邮箱的界面(admin tabs 仅 settings/groups/forums/plugins)

其他用户可见信息(公开面)

信息 位置
用户名 / 头像 侧栏用户卡 sidebar_user_card_html
用户组头衔 + 积分 :1236(组名 · 积分 X)
封禁 / 禁言状态 user_state_tag_html()
个人简介 bio sidebar_bio_card_html :1249
TA 的主题 / 回帖列表 topic_index_page(含受限版块内容,即漏洞一)
最新用户(首页侧栏 8 个用户名+头像) :1246

看不到的

  • 邮箱、密码(任何非本人页面)
  • 注册时间 / 积分变动 / 通知记录(tab=notifications 仅本人页允许,:2700/:2704,无 IDOR)
  • IP(系统不落库,仅登录/注册限流临时取用)

六、设计面说明(非漏洞,但值得知悉)

  • 插件安装 = 管理员任意代码执行(by design):plugin_upload()(Plugin.php:1037)、plugin_market_install()(:199) 允许 allow_admin 用户把验证过的 PHP 写入 app/plugins/ 并被 include。上传校验较完善(TOKEN_PARSE 语法检查、<?php+defined('APP_ROOT') 检查、ID 白名单、市场 sha256 校验、防路径穿越)。.ai-rules.md 明确"插件权限等同站点代码"。无任何未授权路径可达插件安装。
  • a=cron 未鉴权(by design):README 明确要求第三方 cron 服务每分钟 GET 访问。攻击者能触发"到期"的插件定时任务;任务租约令牌(lease_token)防并发重复执行,但无法阻止反复提前触发。

七、漏洞联动与修复优先级

联动链

  1. 用漏洞二把自己组的 allow_manage 打开,或把受限版块的 allow_view_groups 置空;
  2. 用漏洞一的搜索/主页/主页 tab 系统性拉取受限内容;
  3. 两个中危组合 = 完整拿到受限版块内容 + 内容管理权限。

修复优先级

  1. 优先修复漏洞一(受限版块泄露)——可批量、可匿名获取受限内容元数据,且属越权访问范畴
  2. 修复漏洞二(权限不一致)——admin_edit_page 的 group/forum 分支改 need_manage()
  3. 修复漏洞三(私信无限流)——加与发帖同级的频率限制
  4. 顺手加固漏洞四、五、六——OPcache 接口加随机锁值校验;IP 默认信任 REMOTE_ADDR;私信发送加 is_muted() 检查

附录:关键代码位置速查

功能 位置
topic_index_data()(漏洞一核心) index.php:2597
forum_page / topic_page 权限检查 index.php:2795 / :2821
admin_edit_page()(漏洞二入口) index.php:3083
save_group() / save_forum() index.php:2338 / :2323
user_notify_page()(私信发送) index.php:2275
create_notification() index.php:910
opcache_refresh_route()(漏洞四) index.php:772
ip_addr()(漏洞五) index.php:742
check() 全局 CSRF index.php:1425
markdown_html() index.php:1786
h() 转义函数 index.php:267
路由表 core_routes() index.php:3153
插件上传 plugin_upload() Plugin.php:1037
cron 路由 cron_route() Cron.php:205
正文完
 0
评论(没有评论)