安全加固与 PHP 8 兼容
Why
Xiuno BBS 4.0 核心代码使用 mysql_* 等已在 PHP 7.0 移除的函数,无法在 PHP 8.0+ 运行;同时存在密码明文 MD5+salt 存储、无 CSRF 防护、无登录失败限制等高危安全漏洞。本阶段目标是让核心代码在 PHP 8.0+ 无错运行并修复所有已知高危安全问题。
What Changes
修复 PHP 8 不兼容语法(
&new、each()、create_function()、preg_replace /e等)BREAKING 移除
db_mysql.class.php,将mysql驱动默认切换为pdo_mysql;保留db_*全局函数签名不变BREAKING
user表新增password_hash字段,登录验证逻辑改为优先password_verifyuser表新增login_attempts、last_login_ip、last_login_time、banned_until字段新增
user_login_log表所有 POST 表单新增
csrf_token隐藏字段,服务端统一验证输出转义:默认
htmlspecialchars($var, ENT_QUOTES | ENT_HTML5),富文本经HTMLPurifier新增
CsrfService、LoginSecurityService服务类新增
install/upgrade_phase1.sql升级脚本
Impact
Affected specs: 数据库层、用户认证、表单处理、输出渲染
Affected code:
xiunophp/db_mysql.class.php— 标记废弃,不再默认加载xiunophp/db.func.php—db_new()移除mysqlcasexiunophp/db_pdo_mysql.class.php— 成为默认驱动xiunophp/xiunophp.min.php— 同步移除 db_mysql 代码model/user.func.php— 新增user_login_verify()、user_login_attempt()、user_login_log()route/user.php— 登录逻辑重写,POST 处理前验证 CSRFroute/*.php— 所有 POST 分支前加 CSRF 验证view/htm/*.htm— 所有<form>内加csrf_token隐藏字段index.inc.php— 生成 CSRF token 并存入 sessioninstall/install.sql— 新增字段和表conf/conf.default.php— 新增安全配置项xiunophp/xn_html_safe.func.php— 增强为 HTMLPurifier 封装
ADDED Requirements
Requirement: PHP 8 语法兼容
系统 SHALL 在 PHP 8.0 ~ 8.3 下无错运行,不使用任何已移除函数或语法。
Scenario: 移除 mysql_* 函数
WHEN 配置
db.type = 'mysql'THEN 系统输出明确错误提示 "mysql driver removed, please use pdo_mysql",并拒绝启动
Scenario: 移除 &new 语法
WHEN 代码中出现
$a = &new C()模式THEN 替换为
$a = new C()
Scenario: each() 替换
WHEN 代码中使用
each()函数THEN 替换为
foreach或key()/current()/next()组合
Requirement: PDO 数据库驱动迁移
系统 SHALL 将 db_mysql.class.php 标记为 @deprecated,默认使用 db_pdo_mysql.class.php,保留所有 db_* 全局函数签名不变。
Scenario: 默认驱动切换
WHEN
conf.php中db.type为mysqlTHEN 自动映射为
pdo_mysql并记录警告日志
Scenario: db_* 函数兼容
WHEN 插件调用
db_find()、db_exec()等函数THEN 行为与旧版完全一致,签名不变
Requirement: 密码哈希迁移
系统 SHALL 支持从 MD5+salt 到 password_hash() 的渐进式迁移。
Scenario: 新用户注册
WHEN 用户注册时
THEN 使用
password_hash($password, PASSWORD_DEFAULT)存储,password_hash字段写入值,password和salt字段保留但不再使用
Scenario: 旧用户登录自动升级
WHEN 用户登录且
password_hash字段为空THEN 先用旧方式
md5($password.$salt)验证;验证成功后自动用password_hash()生成新哈希写入password_hash字段
Scenario: 新密码验证
WHEN 用户登录且
password_hash字段非空THEN 使用
password_verify($password, $password_hash)验证
Scenario: 批量迁移脚本
WHEN 管理员运行
cli/migrate_passwords.phpTHEN 所有
password_hash为空且password非空的用户被标记为待迁移(不批量解密,仅标记),下次登录时自动升级
Requirement: CSRF Token 验证
系统 SHALL 为所有 POST/PUT/DELETE 请求验证 CSRF Token。
Scenario: Token 生成
WHEN 用户访问任何页面
THEN 系统在 session 中生成随机
csrf_token,并通过$header['csrf_token']传递给模板
Scenario: 表单提交
WHEN 用户提交 POST 表单
THEN 服务端验证
$_POST['csrf_token']与$_SESSION['csrf_token']一致;不一致则返回错误
Scenario: AJAX 请求
WHEN 前端通过 AJAX 发送 POST 请求
THEN 请求头
X-CSRF-Token或参数csrf_token必须携带有效 token
Scenario: Token 轮换
WHEN CSRF 验证成功后
THEN 不立即轮换 token(保持会话级 token),避免多标签页问题
Requirement: 输出转义
系统 SHALL 对所有输出进行安全转义。
Scenario: 纯文本输出
WHEN 模板输出用户提交的纯文本
THEN 使用
htmlspecialchars($var, ENT_QUOTES | ENT_HTML5, 'UTF-8')转义
Scenario: 富文本输出
WHEN 模板输出帖子内容等富文本
THEN 经过
HTMLPurifier过滤,仅允许白名单标签和属性
Requirement: 登录失败限制
系统 SHALL 限制登录失败次数,防止暴力破解。
Scenario: 失败计数
WHEN 用户登录失败
THEN
login_attempts字段 +1,记录last_login_ip和last_login_time
Scenario: 账户锁定
WHEN
login_attempts>= 配置的最大次数(默认 5)THEN 账户锁定至
banned_until时间(默认 15 分钟),期间拒绝登录
Scenario: 登录成功
WHEN 用户登录成功
THEN
login_attempts重置为 0,banned_until清空,写入user_login_log表
Scenario: 登录日志
WHEN 每次登录(成功或失败)
THEN 写入
user_login_log表(uid, ip, time, success, user_agent)
Requirement: 升级脚本
系统 SHALL 提供 SQL 升级脚本,支持从 4.0 无损升级。
Scenario: 执行升级
WHEN 管理员执行
install/upgrade_phase1.sqlTHEN
user表新增password_hash、login_attempts、last_login_ip、last_login_time、banned_until字段;创建user_login_log表;所有新字段有合理默认值,不影响现有数据
MODIFIED Requirements
Requirement: db_new() 函数
原实现支持 mysql 类型。修改后:mysql case 移除,映射为 pdo_mysql 并记录日志。函数签名 db_new($dbconf) 不变。
Requirement: user_login 验证逻辑
原实现使用 md5($password.$user['salt']) == $user['password']。修改后:优先 password_verify(),回退旧方式并自动升级。登录前检查 banned_until。
Requirement: POST 路由处理
原实现直接处理 POST 参数。修改后:所有 POST 分支在业务逻辑前调用 csrf_check()。
REMOVED Requirements
Requirement: db_mysql 驱动
Reason: mysql_* 函数在 PHP 7.0 已移除,无法在 PHP 8.0+ 运行
Migration: conf.php 中 db.type 从 mysql 改为 pdo_mysql;db_mysql.class.php 文件保留但标记 @deprecated,不再被 db_new() 加载
作者:贰先生
链接:https://juejin.cn/post/7647463706607009835
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


