参数化查询是唯一可靠的sql注入防护方式,因其将sql结构与数据彻底分离,使用户输入仅作为值不参与语法构建;其他如转义、长度限制、正则过滤等均为无效或辅助手段。

单纯靠引号转义和长度限制防 SQL 注入,基本无效。这两招在真实攻击场景中极易被绕过,反而会给人“已防护”的错觉,埋下更大风险。
为什么 mysqli_real_escape_string 不足以拦截含特殊符号的注入
该函数只对单引号 '、双引号 "、反斜杠 \、NULL 字符等做简单转义,但攻击者可直接避开引号:用 CHAR(39) 构造单引号,用 0x27 十六进制字面量,或用内联注释 /*!50000SELECT*/ 混淆关键词。MySQL 会照常解析这些表达式,而 mysqli_real_escape_string 完全不处理它们。
常见错误现象:
- 前端输入
admin' OR 1=1 --被转义后看似安全,但攻击者改用admin'/**/OR/**/1=1就能绕过空格过滤 - 后端用
strlen($input) 限制长度,但攻击者用 URL 编码(如 <code>%0a换行符)或宽字节字符(如%E2%80%98左单引号)让实际字节数超标却逃过检测
参数化查询才是唯一可靠的防线
预编译语句把 SQL 结构和数据彻底分离,数据库引擎在解析阶段就固定执行逻辑,用户输入永远只是值,不会参与语法构建。这是防御所有变种注入(含特殊符号、编码、注释、大小写混淆)的底层保障。
实操建议:
- PHP 中必须用
mysqli_prepare()+mysqli_stmt_bind_param(),禁用mysqli_query()拼接字符串 - Python 的
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)),%s占位符不可替换为str.format()或f-string - Java 的
PreparedStatement必须调用setString()等绑定方法,不能用statement.executeUpdate("... " + userInput + " ...") - MyBatis 中只允许用
#{},严禁用${}直接拼接
长度限制与正则过滤只能作为辅助手段
它们无法替代参数化,但可在早期拦截明显异常输入,降低误报率和日志噪音。
使用场景:
- 手机号字段强制匹配
/^\d{11}$/,非数字直接拒收 —— 这比事后转义更干净 - 用户名限制为
/^[a-zA-Z0-9_]{3,16}$/,既防注入也防 XSS,还能统一前端校验逻辑 - 对自由文本字段(如评论)设硬性长度上限(如 500 字符),配合 WAF 规则拦截
UNION SELECT类长 payload
注意:所有正则必须在服务端重复校验,前端 JS 校验可被绕过;长度限制按 UTF-8 字节数而非字符数判断,避免宽字节截断漏洞。
真正难防的不是带特殊符号的注入,而是开发者以为“已经转义了”“长度卡住了”,结果在关键路径上仍用字符串拼接执行 SQL —— 那些地方,一个 CONCAT() 或 INFORMATION_SCHEMA 查询就能直接拖库。











