参数化查询是唯一可靠方案:它将sql结构与用户数据彻底分离,数据库预编译时锁定语法树,使输入仅作为值处理,从根本上防御所有注入变种。

参数化查询是唯一可靠方案
直接拼接字符串构造 SQL 语句,无论加多少层 replace() 或 mysql_real_escape_string(),都挡不住 CHAR(39)、0x27、内联注释 /*!50000SELECT*/ 这类绕过手段。参数化查询把结构和数据彻底分离,数据库在预编译阶段就锁定语法树,用户输入永远只是值——这是防御所有变种注入的底层机制。
实操要点:
- Python 的
cursor.execute("SELECT * FROM t WHERE name = %s", (user_input,)),严禁用.format()或f-string拼接 - Java 的
PreparedStatement必须调用setString()等绑定方法,不能写statement.executeUpdate("... " + userInput + " ...") - MyBatis 中只允许用
#{},禁用${}直接插值 - SQL Server 存储过程里传入
@param,不等于自动安全——若该参数用于LIKE,仍需额外处理通配符
LIKE 查询里的 % 和 _ 必须显式转义
参数化能防注入,但拦不住 LIKE 自身的通配符逻辑。传入 'user_name' 到 WHERE col LIKE @pattern,下划线仍会被当作单字符通配符匹配,导致查出 userXname 这类无关结果。
正确做法不是改参数值,而是声明转义规则:
- MySQL / PostgreSQL / Oracle:统一用
ESCAPE '!',例如WHERE name LIKE '%user!_name%' ESCAPE '!';避免用反斜杠,因客户端可能提前吞掉 - SQL Server:推荐方括号语法
WHERE name LIKE '%[_]%',或ESCAPE '',但必须确保整个表达式一致 - PostgreSQL 的
ILIKE仍按通配符处理_,~(正则)才把它当字面量——别混用 - 应用层预处理可选
re.escape(),但要同步在 SQL 中声明对应ESCAPE字符,否则白做
单引号和标识符特殊字符别靠手写替换
把 O'Reilly 替成 O''Reilly 看似简单,但 MySQL 在 NO_BACKSLASH_ESCAPES 开启时会把 \' 当非法语法;Oracle 不支持双引号字符串;SQLite 双引号行为依赖 PRAGMA double_quote_literals。手动处理极易因数据库模式差异崩掉。
真正省心的做法:
- 所有字符串值一律走参数占位符,如
INSERT INTO t (name) VALUES (?),由驱动自动适配引号规则 - 表名/列名含空格或特殊符号(如
[Order Details]),用标准分隔符:[](SQL Server)、"(PostgreSQL / SQL Server)、反引号(MySQL),而非拼接 - 存储过程参数或
AddNew方法本身已隔离数据,无需再对单引号做replace()—— 但前提是没在内部拼 SQL
长度限制和正则过滤只是辅助,不是防线
设用户名最多 16 字符、手机号必须 /^\d{11}$/,这些能在早期拦截明显异常输入,降低日志噪音,但完全无法替代参数化。
常见失效场景:
- 攻击者用 URL 编码
%0a或宽字节%E2%80%98绕过strlen()字符数检查 - 前端 JS 正则校验被跳过,服务端没复核就直接拼 SQL
- 自由文本字段(如评论)设了 500 字符上限,但攻击者用短 payload 配合
INFORMATION_SCHEMA拖库 - 最危险的是:开发者以为“长度卡住了”“单引号转义了”,结果在某个导出报表的临时 SQL 路径里,又悄悄用
CONCAT()拼了一次字符串
真正难防的从来不是特殊符号本身,而是人误判了防护边界——只要存在一处字符串拼接执行 SQL,前面所有转义、过滤、长度控制,全归零。











