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=...> 会被变成 <img onerror=...>,无法构造标签或属性逃逸。插件 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)防并发重复执行,但无法阻止反复提前触发。
七、漏洞联动与修复优先级
联动链
- 用漏洞二把自己组的
allow_manage打开,或把受限版块的allow_view_groups置空; - 用漏洞一的搜索/主页/主页 tab 系统性拉取受限内容;
- 两个中危组合 = 完整拿到受限版块内容 + 内容管理权限。
修复优先级
- 优先修复漏洞一(受限版块泄露)——可批量、可匿名获取受限内容元数据,且属越权访问范畴
- 修复漏洞二(权限不一致)——
admin_edit_page的 group/forum 分支改need_manage() - 修复漏洞三(私信无限流)——加与发帖同级的频率限制
- 顺手加固漏洞四、五、六——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 |