sql注入防御的核心是参数化查询,而非输入过滤;必须使用数据库驱动原生支持的预编译机制,杜绝用户输入进入sql解析阶段。
☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜

SQL注入防御的核心不是过滤,是参数化
用字符串拼接构造 SQL 语句,哪怕加了 escape_string 或正则替换关键词,都挡不住绕过手段。真正有效的防线只有一条:让用户输入永远不进入 SQL 解析阶段。这意味着必须用数据库驱动原生支持的参数化查询(prepared statement),而不是靠应用层“清理”输入。
常见错误现象:mysqli_query($conn, "SELECT * FROM users WHERE id = '" . $_GET['id'] . "'") 这类写法,无论后端怎么 trim、strip_tags、甚至 blacklist ' OR 1=1 --,攻击者总能用十六进制编码、大小写混用、注释符变形等方式绕过。
- PHP 的
mysqli_prepare()和PDO::prepare()是唯一推荐路径;mysql_*函数已废弃且不支持参数化 - Node.js 用
pg.query('SELECT * FROM users WHERE id = $1', [req.query.id]),不是pg.query(`SELECT * FROM users WHERE id = ${req.query.id}`) - Python 的
sqlite3.execute("SELECT * FROM users WHERE name = ?", (name,))中的?或:name占位符由驱动解析,变量值绝不会被当 SQL 执行
ORM 并不自动免疫 SQL 注入
很多人以为用了 Django ORM 或 Laravel Eloquent 就高枕无忧,其实不然。这些框架在常规链式调用(如 User.objects.filter(name='xxx'))中确实安全,但一旦出现原始 SQL 拼接或动态字段名/表名,风险立刻回归。
使用场景:需要动态排序字段、多租户分表、或 legacy 系统迁移时临时兼容旧逻辑。
- Django 的
extra()、raw()、extra_tables等方法若拼接用户输入,等同于裸写 SQL - Laravel 的
DB::select(DB::raw("SELECT * FROM {$table}"))中的$table若来自请求参数,就是典型漏洞点 - Java MyBatis 的
${xxx}是字符串替换,#{xxx}才是预编译参数——这个区别必须刻在脑子里
WHERE 条件里用 IN 时参数数量不确定怎么办
这是参数化最常卡壳的地方:SQL 要求占位符数量固定,但前端可能传回 1 个或 50 个 ID。硬编码 IN (?, ?, ?, ...) 不现实,也不该用字符串拼接生成占位符。
正确做法是根据输入数组长度动态生成对应数量的 ?,再把数组整体传入参数列表。不能省略这一步,否则要么性能差(循环单条查),要么又退回到拼接。
- PHP PDO 示例:
$ids = [1, 5, 9]; $placeholders = str_repeat('?,', count($ids) - 1) . '?'; $stmt = $pdo->prepare("SELECT * FROM posts WHERE id IN ($placeholders)"); $stmt->execute($ids); - Python sqlite3 不支持多值占位符,需用
executemany分批或改用in_()(SQLAlchemy)这类封装好的安全方法 - 注意 PostgreSQL 对
IN参数数量有限制(默认 65535),超量时得改用UNNEST(ARRAY[...])配合 JOIN
除了参数化,还有哪些必须同步做的底线措施
参数化是核心,但不是全部。比如错误信息泄露表结构、数据库账号权限过大、或未校验输入类型,都会放大攻击后果。
容易踩的坑:认为“我用了 prepare 就万事大吉”,结果调试时开着 display_errors = On,一条报错直接暴露字段名和表名;或者用 root 账号连库,被注入后执行 SELECT LOAD_FILE('/etc/passwd')。
- 关闭生产环境的详细错误输出,用日志记录而非页面返回;MySQL 设置
sql_mode = 'STRICT_TRANS_TABLES'防止宽泛类型隐式转换 - 数据库账号仅授予最小必要权限:
SELECT, INSERT, UPDATE,禁用FILE、LOAD DATA、EXECUTE等高危权限 - 对数字型参数强制类型转换:
$id = (int)$_GET['id'];再传入参数化查询——这不是替代方案,而是双保险
最麻烦的其实是动态表名、列名、ORDER BY 字段这类无法参数化的场景。它们没法用占位符,只能靠白名单严格校验,比如把允许的排序字段限定为 ['created_at', 'title', 'status'],其余一概拒绝。这点容易被忽略,但恰恰是真实项目中最常出问题的地方。










