欢迎来到 Xiuno BBS

Xiuno BBS 重构记录贴(一)安全加固与 PHP 8 兼容

# 安全加固与 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()` 加载

1 0 2
复制成功

回复 (2)

leo 6天前
#2
支持,厉害[em_55]
chaopu 5天前
#3
支持一下,xiuno有了新生机