结论:防 SQL 注入唯一可靠的做法是把 SQL 语句和数据彻底分开——参数化查询让用户输入永远只作为“值”进入数据库,而不是作为“语法”被解析;字符过滤、转义函数、正则黑名单都只是概率防御,终究有漏网的写法。
什么是参数化查询?为什么它能挡住注入
参数化的本质是“先编译、后填值”:数据库先把带占位符的 SQL 编译成执行计划,参数到达时只按值填入,不会被重新解析。所以哪怕用户提交的是 ' OR '1'='1,它也只是被当成一串普通字符去匹配,不会变成条件。
对比一下就很清楚。拼接写法 "SELECT * FROM post WHERE title LIKE '%$kw%'" 里,$kw 参与了 SQL 语法构建;而 PDO 的写法把语句和数据分成两次投递:
$stmt = $pdo->prepare('SELECT id,title FROM post WHERE title LIKE :kw AND status = 1');
$stmt->execute([':kw' => '%' . $kw . '%']);
这也是为什么参数化能一并解决数字型注入——数字型注入根本不需要引号闭合,id=1 UNION SELECT ... 这类 payload 靠 addslashes 是拦不住的。结论:只要变量进入了 SQL 文本,就存在注入面;只有参数化能把这条面彻底关掉。
论坛里哪些位置最容易出现注入
论坛的注入入口高度集中在“筛选、排序、分页、搜索”这四类参数上,因为它们往往被当成动态 SQL 片段拼接。典型清单:搜索关键词、版块 ID 与子分类 ID 筛选、order by 字段与升降序、页码 page、帖子 ID 与回复楼层、用户组权限查询。
其中 ORDER BY 和 LIMIT 两个是参数化的“盲区”:MySQL 不允许用占位符代替列名、表名、排序方向,LIMIT 在关闭模拟预处理时必须显式绑类型。做法是白名单映射,不做任何字符串拼接:
$allow = ['new' => 'created_at', 'hot' => 'views'];
$col = $allow[$sort] ?? 'created_at';
$dir = ($order === 'asc') ? 'ASC' : 'DESC';
$sql = "SELECT * FROM post ORDER BY $col $dir LIMIT :off, :num";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
$stmt->bindValue(':num', $perPage, PDO::PARAM_INT);
IN (...) 同理,占位符数量要按数组长度动态生成:implode(',', array_fill(0, count($ids), '?')),再把数组整体 execute($ids)。LIKE 查询要额外转义 %、_、\ 三个字符,否则用户可以构造超宽匹配拖慢查询。
参数化配置上还有哪些必须踩实的点
四个工程细节决定参数化是否真的生效。第一,关闭模拟预处理:PDO::ATTR_EMULATE_PREPARES => false,让 MySQL 5.7+ 走真正的服务端预处理,而不是 PDO 在本地做字符串转义(后者本质还是转义)。第二,字符集统一 utf8mb4 并显式 SET NAMES,避免 GBK 宽字节注入的经典坑。第三,关闭异常回显:PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION 配合生产环境 display_errors=Off,别把带表结构的报错吐到页面上,那是给攻击者送字典。第四,数据库账号按最小权限分配,业务账号只给 SELECT/INSERT/UPDATE/DELETE,不给 DROP、FILE、INTO OUTFILE。
还有一类容易被忽略的问题:二次注入。用户输入的恶意内容被“安全地”存进库里,之后被另一处代码取出再拼进 SQL 时,参数化的保护就失效了。解决方案只有一个——所有查询一律参数化,不允许“这条语句我觉得安全”的例外。
自己写论坛插件时,SQL 该怎么写
结论:插件里的 SQL 安全由插件作者自己负责,系统不会替你转义钩子传进来的参数。
Clara BBS 是运行时钩子体系,共 156 个钩子,插件放在 content/plugins 目录,保存即生效、无需编译。这套机制的便利之处是改完立刻能验证,风险也在这里:钩子里拿到的 $_GET、$_POST、用户 ID、版块 ID 都是不可信数据,直接拼进查询就是现成的注入点。
具体做法是三条:所有外部输入一律通过 prepare + execute 传入,不出现变量拼接;涉及表名、列名、排序方向时用数组白名单映射,命中不了就走默认值;新增表的建表语句放进插件的安装流程,字符集统一 utf8mb4。系统本身的安全基线覆盖的是 Web 层——全站 CSRF 防护、密码 bcrypt 存储、输出转义、上传文件类型白名单加图片二次校验、登录失败 5 次锁定 15 分钟并留审计日志——这些挡的是跨站请求伪造、XSS、撞库和恶意文件,挡不住你自己写出来的拼接 SQL。
一句话收尾:参数化不是“更安全的写法之一”,而是 SQL 注入的终止条件。凡是变量进 SQL 的地方都给占位符,凡是标识符进 SQL 的地方都给白名单,两条都做到,注入面就归零了。