Xiuno BBS 重构记录贴(一)安全加固与 PHP 8 兼容
贰先生 9小时前

# 安全加固与 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_verify`
- `user` 表新增 `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()` 移除 `mysql` case
- `xiunophp/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 处理前验证 CSRF
- `route/*.php` — 所有 POST 分支前加 CSRF 验证
- `view/htm/*.htm` — 所有 `<form>` 内加 `csrf_token` 隐藏字段
- `index.inc.php` — 生成 CSRF token 并存入 session
- `install/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``mysql`
- **THEN** 自动映射为 `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.php`
- **THEN** 所有 `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.sql`
- **THEN** `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()` 加载
最新回复 (0)
全部楼主
返回
贰先生
一级用户组
6
主题数
11
帖子数
扫码访问