本篇教你两件事:SQL 注入在代码里到底长什么样(六种成因),以及每一类怎么修才修得干净。学完你应该能做到:随便给你一段拼 SQL 的代码,你能说出它属于哪类成因、怎么绕、怎么修。
先修知识
- SQL:操作数据库的指令语言。类比:你写给仓库管理员的取货单,管理员(数据库)照单执行,不思考单子合不合理。
- SQL 注入:攻击者把 SQL 语法片段混进正常输入,让数据库把”数据”当”指令”执行。类比:取货单备注栏里写”顺便把保险柜打开”,管理员照做了。
- 拼接:用
+(Java)或.(PHP)把变量串进 SQL 字符串。本篇里它是”万恶之源”。 - 预编译 / 参数化查询:SQL 语句先交给数据库编译成固定骨架(值的位置用
?占位),数据随后单独传入。类比:先印好表格模板,再往格子里贴照片——照片内容再奇怪,也不可能变成表格的格子线。 - 占位符:SQL 骨架里给数据预留的位置,
?(JDBC/PDO)或:name(PDO 命名占位符)。 - 标识符:表名、列名、
ORDER BY字段这类”结构名字”,和”值”是两种东西——占位符只能占值的位置,占不了标识符的位置,这是理解很多漏洞的关键。 - 转义:给特殊字符(如单引号)前面加反斜杠(
'→\'),让它失去语法意义、退化为普通字符。 - 宽字节注入:利用 GBK 等多字节编码,让一个汉字”吃掉”转义用的反斜杠,使单引号逃逸。后面有逐字节拆解。
- 二次注入:恶意数据先安全地存进数据库,之后被取出再次拼进 SQL 时才引爆。
- ORM:对象关系映射框架(MyBatis、Hibernate、Laravel Eloquent),让你用对象操作代替手写 SQL。
- 存储过程:预存在数据库里的一段 SQL 程序,可以被调用。它内部也能写动态 SQL。
- WAF / RASP:应用防火墙 / 运行时自保护,属于”外挂”防御,挡不住代码本身有洞。
什么是 SQL 注入
SQL 注入是指攻击者把恶意的 SQL 语法片段混入本该是”数据”的输入中,使数据库把它当作”代码”解析执行,从而越权读取、篡改、删除数据,甚至执行系统命令的漏洞。
组成部分
SELECT * FROM user WHERE name = 'admin'
|________语法结构(代码)________| |__数据__|
数据库执行前要先解析这条语句,生成语法树(把字符串拆成”查哪张表、条件是什么”的结构化理解)。关键点在于:解析器无法区分”哪些字符是开发者写的语法、哪些是用户提交的数据”——它只看最终拼出来的完整字符串。
用生活例子理解:你让助理打印一份合同,里面留了一行”乙方:____“让他手写填名字。结果他在横线上写的是”乙方:张三,且本合同全部条款作废”。打印机(数据库)不会分辨哪部分是模板、哪部分是手写的——整页纸都按字面生效。
本质:用户提交的”数据”中包含引号、注释符、关键字,改变了语法树的形状,语义被劫持。
成因分析(代码层)
下面六种成因按”开发者怎么一步步写歪的”展开。每一种都回答三个问题:开发者为什么这么写、漏洞在哪、审计时怎么认出它。
1. 直接字符串拼接
开发者动机:最直接、最省事——“我要查 id 为 X 的新闻,把 X 拼进去不就完了”。新手教程里大量这种写法,很多人第一版代码就这么写的,不知道有危险。
$id = $_GET['id']; // 用户输入,直接取自 URL 参数
$sql = "SELECT * FROM news WHERE id = " . $id; // 用 . 拼进 SQL,连引号都没有
mysql_query($sql); // 原样交给数据库执行数字型参数(id)甚至没有引号包裹,攻击者直接写 1 AND 1=2、1 UNION SELECT ... 即可——不需要闭合任何引号,语法直接续写,是最低级也最常见的形态。
String name = request.getParameter("name");
String sql = "SELECT * FROM user WHERE name = '" + name + "'"; // 字符型,有引号包裹
Statement stmt = conn.createStatement();
stmt.executeQuery(sql);Statement 本身不提供参数绑定能力,拼接是它唯一的使用方式——所以审计时看到 createStatement() 就要高度警惕,见到它基本等于见到注入候选,只需再确认参数可控。
审计识别特征:SQL 字符串里出现 " + 变量 + " 或 ' . $变量 . ';数字型参数裸拼无引号。
2. 动态标识符无法参数化,退回拼接
开发者动机:产品要求”用户可以按价格/时间/热度排序”,排序字段是活的。但表名、列名、ORDER BY 字段属于标识符,不能作为 ? 绑定参数(占位符只能占”值”的位置,数据库语法不允许 ORDER BY ?)。开发者不知道正确做法(白名单),图省事直接拼:
String order = request.getParameter("sort"); // 用户传: id,(select sleep(5))
String sql = "SELECT * FROM product ORDER BY " + order; // 标识符位置直接拼接 ❌这是”用了预编译仍然有注入”的主要来源——很多团队全部查询都参数化了,唯独排序、动态表名这类场景被迫拼接,而这里恰恰没做防护。修复只能靠白名单(见下文修复章节)。
审计识别特征:ORDER BY + 拼接、ASC/DESC 由参数控制、动态表名/列名。全局搜 ORDER BY.*\+ 和 ORDER BY ${(MyBatis)。
3. ORM / 框架的”拼接后门”
开发者动机:“我们用了 MyBatis/Laravel,ORM 天然防注入”——这是最流行的错误认知。真相是:框架提供了安全的参数绑定,但同时留了原始 SQL 出口,遇到框架表达能力不够的场景(复杂 LIKE、动态排序、原生函数),开发者顺手就用了那个危险出口。
<!-- MyBatis 的 mapper XML -->
<!-- ✅ #{} → 编译为 ? 占位符,值后传,安全 -->
WHERE name = #{name}
<!-- ❌ ${} → 文本替换,在 SQL 送达数据库前就把变量塞进去,等价于字符串拼接 -->
ORDER BY ${orderField}
WHERE name LIKE '%${keyword}%'// Laravel
DB::select("SELECT * FROM user WHERE name = '$name'"); // ❌ 裸 SQL 拼接
User::whereRaw("name = '$name'")->get(); // ❌ whereRaw 同样拼接审计识别特征:MyBatis 搜 \${(每个出现点都人工确认);Laravel 搜 DB::raw、whereRaw、DB::select;Hibernate 搜 createNativeQuery + 拼接。
口诀:“用了 ORM 就安全”不成立——ORM 只保证你用它提供的绑定 API 时安全。
4. 错误的防御手段——转义被绕过
开发者动机:意识到注入危险了,但不愿意(或不会)改参数化,于是打个补丁:“把单引号转义掉不就安全了?”
// ⚠️ addslashes:把 ' 变成 \',看似堵住了
$id = addslashes($_GET['id']);
$sql = "SELECT * FROM user WHERE id = '$id'";宽字节注入绕过它。当数据库连接编码是 GBK 这类多字节编码时,攻击者提交 %df':
- 输入到达 PHP 时实际是字节序列
0xdf 0x27(%27就是单引号'); addslashes在单引号前插入反斜杠\(0x5c),字节序列变成0xdf 0x5c 0x27(即%df%5c%27);- 数据库连接按 GBK 解码时,按”双字节”规则读:
%df%5c恰好拼成一个合法 GBK 汉字(“運”),反斜杠被当作汉字的低位字节”吃掉”了; - 剩下的
0x27单引号干干净净地逃逸成功 → 注入成立。
根子在哪:转义依赖”转义函数理解的字符集”与”数据库理解的字符集”一致。PHP 按单字节理解、数据库按 GBK 双字节理解,这个前提一旦不成立,转义就出现裂缝。生产环境的编码配置链路很长(PHP 文件编码、连接编码、库编码、表编码),任何一环对不上就出事——这是转义方案被行业淘汰的根本原因。
另外注意:转义对数字型注入天然无效——id=1 and 1=2 里根本没有引号需要转义。
审计识别特征:搜 addslashes、mysql_real_escape_string、str_replace("'"),然后检查:参数是不是数字型(是 → 直接判漏洞);编码是不是 GBK 系(是 → 宽字节可绕)。
5. 二次注入(Second-Order Injection)
开发者动机:入库时规规矩矩做了转义,于是产生一个隐蔽的错误信任假设——“存进我库里的数据肯定是干净的”,之后取出来用时就不再设防。
// —— 注册:转义入库 ——
$u = addslashes($_POST['username']); // 攻击者注册 admin'-- (注意 -- 后必须带空格)
mysql_query("INSERT INTO user(username) VALUES('$u')");
// 转义只对"这一次"查询的 SQL 解析生效,库里落盘的仍是原始值 admin'--
// —— 改密码:从库里取出用户名直接拼接 ❌ ——
$row = mysql_fetch_assoc(mysql_query("SELECT username FROM user WHERE id=$uid"));
$sql = "UPDATE user SET pwd='$newpwd' WHERE username = '" . $row['username'] . "'";
// 取出的 admin'-- 再次污染语法 → 实际执行 WHERE username = 'admin',改了管理员的密码成因本质是”库里的数据是干净的”这一错误信任假设。类比:你把一瓶贴了”有毒”标签的液体存进公共冰箱(标签在入库时被撕了),别人取出来当可乐用了。
防御原则:数据只要出库再用,一律按不可信数据处理。 审计时跟踪每一个 SELECT 取出来的字段的去向——它会不会进入下一条 SQL?
6. 存储过程内的动态 SQL
开发者动机:应用层全部上了预编译,自觉得很安全;但部分逻辑写在数据库存储过程里,存储过程内部又玩起了字符串拼接——防线做在了应用层,漏洞沉到了数据库层。
-- 存储过程内部 ❌
SET @sql = CONCAT('SELECT * FROM user WHERE name = ''', p_name, '''');
PREPARE stmt FROM @sql; -- 拼好的整串在这里编译,p_name 里的注入在此生效
EXECUTE stmt;外层应用把 p_name 规规矩矩参数化传进存储过程也没用——存储过程内部 CONCAT 一拼、再 PREPARE,参数照样变成了语法。
审计识别特征:项目里用了存储过程时,必须把存储过程源码也要来审(它不在应用代码仓库里,容易漏);搜 CONCAT + PREPARE/EXECUTE IMMEDIATE 组合。
成因统一归因
| 成因 | 共同本质 |
|---|---|
| 字符串拼接 | 数据进入语法结构 |
| 动态标识符拼接 | 标识符无绑定机制,退回拼接 |
ORM raw / ${} 滥用 | 绕过框架的绑定通道 |
| 转义防御 | 依赖字符集一致性,前提脆弱 |
| 二次注入 | 信任库内数据 |
| 存储过程拼接 | 拼接位置下沉到数据库层 |
所有成因收敛为两条根因:
- 数据以拼接方式参与 SQL 语法构造(而不是以绑定方式作为值传入);
- 对输入或中间数据做了”它不会有 SQL 语法字符”的信任假设(URL 参数、Cookie、HTTP Header、库内数据全部算不可信输入)。
记住这两条,新出现的任何花式注入变种你都能归类。
SQL 注入代码修复
注入的本质是代码与数据没有分离,防御的根本思路只有一个:让攻击者输入的内容永远被当作”数据”,而不是 SQL 语法。
1. 参数化查询 / 预编译(根治手段)
原理再讲透一点:预编译把一次查询拆成两步——
- 先把含占位符的 SQL 骨架发给数据库编译:
SELECT * FROM user WHERE id = ?。数据库在这一步就定死了语法树:这是一个 SELECT,条件是 id 等于”某个值”; - 再把数据单独传给那个
?。此时语法树已经编译完成,数据只能进”值”的槽位——攻击者输入1 or 1=1也只会被当成一个字面量去比较,数据库不会再回头解析它。
类比:考试答题卡。题目和选项格子是印死的(编译好的语法树),你只能在格子里涂(填数据)。你在格子里写”此题满分”也没用——阅卷机只认格子里的涂点。
Java:
// ✅ PreparedStatement
String sql = "SELECT * FROM user WHERE id = ?"; // 骨架里没有变量
PreparedStatement ps = conn.prepareStatement(sql); // 先编译
ps.setInt(1, userId); // 后传值,setInt/setString 按类型选PHP(PDO):
// ✅ 命名占位符 + 显式类型绑定
$stmt = $pdo->prepare('SELECT id, username, email FROM users WHERE status = :status AND role = :role');
$stmt->bindValue(':status', $status, PDO::PARAM_INT); // 明确告诉 PDO 这是整数
$stmt->bindValue(':role', $role, PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll();PDO 必须关闭模拟预处理
PDO::ATTR_EMULATE_PREPARES => false——模拟预处理(pdo_mysql 驱动默认开启)不是真预编译:它会在 PHP 客户端把参数转义后拼进 SQL 再发送,仍存在宽字节等编码绕过风险(见案例二)。关闭后才是数据库服务端真预编译。
MyBatis:
<!-- ✅ #{} → 底层是 PreparedStatement 占位符 -->
WHERE name = #{name}
<!-- ❌ ${} → 字符串拼接,必须改;实在要动态排序走白名单 -->
ORDER BY ${orderField}预编译防不住的场景(面试题眼):
| 场景 | 原因 | 修复 |
|---|---|---|
| 动态表名/列名/ORDER BY | 标识符不能作为绑定参数 | 白名单枚举映射 |
| LIKE 模糊查询 | %、_ 是 LIKE 语法内通配符,参数化只管得住语法管不住通配符语义 | 绑定参数 + 转义参数内的 %_ |
| 二次注入 | 出库数据再次拼接 | 所有使用点都参数化 |
| 存储过程内动态 SQL | 拼接发生在数据库内部,应用层参数化够不着 | 存储过程内同样参数化 |
LIKE 场景展开说一句:用户搜关键词 % 本身不破坏语法,但能把模糊查询变成全表扫描(性能攻击);正确做法是参数化绑定之后,再把参数里的 %、_ 转义为 \%、\_,并在 SQL 里声明 ESCAPE '\\'。
2. 输入校验(纵深防御,不能替代参数化)
白名单优于黑名单。黑名单是”列出坏蛋”,永远列不全(大小写、编码、注释变形……);白名单是”只放好人进来”,枚举空间有限,天然完备。
- 数字就强制
intval()/Integer.parseInt(),不要试图”过滤掉单引号”——把输入变成数字,比把数字里的坏字符挑出去靠谱一万倍; - 枚举值(排序字段、状态、表名)走白名单映射表,用户传 key、代码查表换成真实标识符,不在表里就用默认值;
- 数据长度严格限制,能在一定程度上拦截过长的注入 payload。
// ✅ 排序字段白名单:用户只能传 key,真实列名由服务端决定
String sort = request.getParameter("sort");
Map<String, String> allowed = Map.of("time", "create_time", "hot", "view_count");
String orderField = allowed.getOrDefault(sort, "id"); // 不在白名单用默认值兜底注意这个设计的精妙处:攻击者输入的任何内容都不会出现在最终 SQL 里——SQL 里出现的只有 Map 的 value,那是开发者自己写死的字符串。这就是”输入”和”语法”的彻底隔离。
3. 转义(最后的兜底,坑很多)
- PHP 的
addslashes()属于上一代方案,GBK 编码下可被%df'宽字节绕过(成因 4 有逐字节拆解); mysql_real_escape_string()必须配合mysql_set_charset()使用——mysql_set_charset()会同步修改客户端库的字符集认知;而SET NAMES只是一条 SQL,只改了服务器端认知,客户端转义时仍按旧字符集,两侧不一致仍可宽字节绕过;- 能用参数化就不要用转义;转义只应出现在无法改造的遗留代码兜底中。
相关:sql注入过滤绕过(转义/过滤被绕过的具体手法)
4. 业务层 / 数据库层防御
代码修完还不够,纵深防御每一层都能降低”修漏一处”的代价:
- 最小权限原则:应用账号禁止
FILE、SUPER权限,按库/表授权,只读业务不给写权限——注入真被打穿时,攻击者拿到的也只是一个低权限会话; - 错误处理:关闭 SQL 报错回显(PHP
display_errors=Off,Java 全局异常兜底),防止报错注入直接泄露库结构; - 统一编码:全站 UTF-8(MySQL 里用
utf8mb4,utf8是残血版三字节实现),上下层编码不一致是宽字节注入的温床; - 数据库访问收口:统一 DAO/SDK 封装,禁止业务代码裸连数据库拼接 SQL——让所有 SQL 经过一个入口,审计只需要查这一个入口;
- 敏感数据加密/哈希存储:即使被拖库也降低危害(缓解措施);
- WAF/RASP 仅作应急缓解:可拦扫描器和通用 payload,但可被绕过(编码、分块、等价语法变形),不能替代代码修复。
完整修复案例(审计 → 修复闭环)
案例一:MyBatis ${} 动态排序注入
漏洞代码(逐行注释):
<select id="listProducts" resultType="Product">
SELECT * FROM product WHERE status = #{status}
<!-- #{status} 走占位符,安全;但下面两个 ${} 是文本替换 -->
ORDER BY ${orderField} ${orderDir}
</select>// Controller:sort/dir 直接取自请求参数,零校验
List<Product> list = productMapper.listProducts(
status, req.getParameter("sort"), req.getParameter("dir"));数据流分析(逆向回溯):
- Source:
req.getParameter("sort")/("dir"),URL 参数完全可控; - 中间处理:Controller 没有任何校验,原样传给 Mapper;
- Sink:Mapper XML 中
${orderField}做文本替换,拼进ORDER BY子句。
#{} 只保护了 status,两个 ${} 完全裸奔。
攻击怎么发生:ORDER BY 位置后面不能直接 UNION SELECT 回显结果,攻击者改用盲注,例如传:
sort=id,(select if(substr(database(),1,1)='s',sleep(3),0))
逐段拆解:
id,—— 合法开头,让排序语法成立;(select if(...))—— 注入的子查询表达式,作为第二个排序键;substr(database(),1,1)='s'—— 取当前库名第一个字符,猜它是不是s(一次猜一个字符);sleep(3)/0—— 猜对了就睡 3 秒,猜错了立即返回。页面响应有没有慢 3 秒,就是攻击者的”回显”——库名就这样一个字符一个字符被磨出来。
审计结论:${} 出现在 ORDER BY 是典型高危场景;排序字段属于标识符,无法改 #{}(ORDER BY ? 语法不合法),修复只能靠白名单。
修复(白名单映射 + 方向枚举):
private static final Map<String, String> SORT_FIELDS = Map.of(
"time", "create_time", // 用户传 time → 实际用 create_time
"price", "price",
"hot", "view_count"
);
String field = SORT_FIELDS.getOrDefault(req.getParameter("sort"), "id"); // 非法输入兜底为 id
String dir = "desc".equalsIgnoreCase(req.getParameter("dir")) ? "DESC" : "ASC"; // 方向只许二选一ORDER BY ${field} ${dir} <!-- 此处 ${} 的内容已被白名单锁死:只可能是 Map 的 value 和 DESC/ASC,安全 -->为什么这里 ${} 反而可以留:${} 本身只是”文本替换”机制,危险与否取决于替换进去的内容谁说了算。白名单之后,内容只可能是开发者写死的几个字符串,攻击者的输入根本到不了 SQL。
案例二:PDO 模拟预处理未关闭导致的宽字节注入
漏洞代码(逐行注释):
// 用了 PDO prepare,表面功夫做足 ⚠️
$pdo = new PDO('mysql:host=127.0.0.1;dbname=shop', 'root', 'root');
// ① DSN 没声明 charset → 客户端库默认 latin1
$pdo->query('SET NAMES GBK'); // ② 只改了服务器侧认知为 GBK,
// 客户端转义函数仍以为自己在用 latin1
$id = $_GET['id']; // ③ Source:用户输入
$stmt = $pdo->prepare('SELECT * FROM goods WHERE id = ?');
$stmt->execute([$id]); // ④ 看似参数化,实际……数据流分析:
id确实走了?占位符——但 pdo_mysql 驱动的PDO::ATTR_EMULATE_PREPARES默认为 true(模拟预处理):参数不是发给数据库服务端编译绑定的,而是在 PHP 客户端转义后拼进 SQL 再整串发送;- 客户端转义依据的是 PDO 客户端字符集(DSN 没写,默认 latin1,按单字节转义);
SET NAMES GBK只是发给服务器的一条 SQL,改了服务器侧认知,不同步客户端库——与mysql_real_escape_string的坑同源;- 于是宽字节条件凑齐:攻击者提交
id=%df' or 1=1%23。
payload 逐段拆解:
%df—— 一个”半汉字”字节,专门用来和转义反斜杠配对;'—— 注入的核心单引号,转义后它前面会被插入\(0x5c),但%df%5c在 GBK 下拼成汉字”運”,反斜杠被吃掉,单引号逃逸;or 1=1—— 逃逸成功后注入的恒真条件;%23—— URL 编码的#,MySQL 行注释符,注释掉语句尾巴。
审计结论:“用了 prepare 就没注入”不成立——模拟预处理 + 字符集不一致时,参数化退化为转义级防御,仍可宽字节绕过。审计 PDO 项目第一眼就看连接代码有没有关模拟预处理、DSN 有没有声明 charset。
修复:
// ✅ DSN 显式声明 utf8mb4(两侧字符集对齐) + 关闭模拟预处理(数据库服务端真预编译)
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4',
'root', 'root',
[PDO::ATTR_EMULATE_PREPARES => false]
);两个改动缺一不可:关模拟预处理让”拼接”这一动作本身消失(根治);DSN 声明 charset 让客户端与服务器字符集认知一致(消灭宽字节前提)。
相关:sql预编译参数化
修复 Checklist
交付修复方案时,对照这张表逐项打钩:
- 全库搜危险 Sink:Java 搜
${、createStatement、executeQuery(拼接;PHP 搜mysql_query、mysqli_query、->query(、->exec(拼接 - 所有拼接点改参数化;不能参数化的标识符(排序字段、表名)改白名单映射
- PDO 关闭模拟预处理(
PDO::ATTR_EMULATE_PREPARES => false)且 DSN 声明charset=utf8mb4;MyBatis 消除所有未加白名单保护的${} - 收紧数据库账号权限(禁
FILE/SUPER,按表授权),关闭错误回显,统一 utf8mb4 - 数据出库再用的地方全部按不可信数据处理(防二次注入)
- 项目里有存储过程的,把存储过程源码一并审掉(搜
CONCAT+PREPARE)
面试题眼速答
| 问题 | 一句话答案 | 展开两三句 |
|---|---|---|
| SQL 注入的本质是什么? | 代码与数据没有分离,数据被当语法解析 | 数据库解析器只看最终拼出来的完整字符串,分不清哪些是开发者写的语法、哪些是用户数据。引号/注释符/关键字改变了语法树形状,语义被劫持。 |
| 为什么预编译能根治注入? | 语法树在数据传入前就编译定死了 | SQL 骨架先编译,? 位置永远只是”值”的槽位;后传的数据不再参与语法解析,输入 1 or 1=1 也只是字面量。类比答题卡:格子印死了,涂什么都只是涂点。 |
| 预编译防不住哪些场景? | 标识符位置、LIKE 通配符、二次注入、存储过程内拼接 | 占位符只能占”值”的位置,ORDER BY ? 不合法,排序字段只能白名单;LIKE 要额外转义参数内 %_;出库再用的数据要重新参数化;存储过程内部也要参数化。 |
| 为什么排序字段注入不能靠参数化修? | ORDER BY ? 语法不合法,标识符不能绑定 | 修复用白名单映射:用户传 key(如 time),服务端 Map 查表换成真实列名(create_time),不在表里用默认值。攻击者输入永远不会出现在 SQL 里。 |
| 宽字节注入的原理? | 多字节编码把转义反斜杠”吃掉” | 提交 %df',addslashes 插入 \(0x5c)变成 %df%5c%27;GBK 双字节解码把 %df%5c 认作汉字”運”,反斜杠消失,单引号逃逸。根因是转义函数与数据库字符集认知不一致。 |
mysql_real_escape_string 和 SET NAMES 的坑? | SET NAMES 只改服务器认知,不同步客户端转义字符集 | 客户端仍按旧字符集(如 latin1)转义,服务器按 GBK 解析,宽字节前提成立。必须用 mysql_set_charset() 让客户端库同步认知。 |
| 为什么转义方案被淘汰? | 依赖字符集一致性这个脆弱前提,且对数字型注入天然无效 | 编码链路长(文件/连接/库/表编码),任一环节不一致就裂缝;数字型注入根本没有引号可转义。参数化不依赖任何前提,是结构性安全。 |
| 什么是二次注入,怎么防? | 恶意数据入库时被转义”洗白”,出库再拼接时引爆 | 入库转义只对当次 SQL 解析生效,落盘的是原始值。防御原则:数据出库再用一律按不可信处理,所有使用点参数化,而不是只在入库点设防。 |
| “用了 ORM 就安全”对吗? | 不对,ORM 只保证绑定 API 安全 | MyBatis 的 ${}、Laravel 的 whereRaw/DB::raw、Hibernate 的 createNativeQuery 拼接都是框架留的原始 SQL 出口。审计要逐个搜这些关键字。 |
| PDO 用了 prepare 为什么还可能有注入? | 模拟预处理开启时参数仍在客户端拼接 | pdo_mysql 默认 ATTR_EMULATE_PREPARES=true,是 PHP 端转义拼接的”假预编译”。配合 SET NAMES GBK 字符集不一致可宽字节绕过。修复:DSN 声明 charset=utf8mb4 + 关模拟预处理。 |
| 修复注入的纵深防御有哪些层? | 参数化为主,白名单补标识符,业务/数据库层兜底 | 数据库账号最小权限、关闭错误回显(防报错注入泄结构)、统一 utf8mb4、DAO 收口统一审计入口、敏感数据加密。WAF/RASP 只是应急缓解,不能替代代码修复。 |
| 白名单为什么优于黑名单? | 黑名单列不全,白名单枚举空间有限天然完备 | 黑名单要对抗大小写、编码、注释变形等无穷变种;白名单只放行已知好的值。数字强制 intval()、枚举走映射表,都是白名单思想。 |