SQL注入主因是字符串拼接用户输入,参数化查询是唯一可靠防线;动态结构如表名/列名需白名单+映射+标识符转义;ORDER BY、limit、GraphQL等非典型入口易被忽略;渗透须结合业务逻辑手工测试。

SQL注入最常出现在拼接查询字符串的地方
只要代码里出现 user_input + " AND status = 1" 这类字符串拼接,基本就等于给攻击者开了后门。PHP 的 mysql_query()、Python 的 cursor.execute("SELECT * FROM users WHERE name = '" + name + "'")、Java 的 Statement.execute() 都是高危操作。
根本原因不是数据库本身不安全,而是应用层把用户输入当成了 SQL 语法的一部分。参数化查询能强制把输入当作“数据”而非“代码”处理,这是唯一可靠的基础防线。
- 所有动态 WHERE 条件、ORDER BY 字段名、LIMIT 数值,只要来自外部输入,就必须走参数化——不能只对 WHERE 值做,而放过
ORDER BY后的字段名 - ORM 如 Django 的
filter(name__contains=xxx)或 SQLAlchemy 的where(User.name.contains(xxx))是安全的;但直接用.extra(where=[f"name LIKE '%{xxx}%'"])就会破防 - 存储过程如果内部用
EXEC(@sql)拼接,照样中招;必须用sp_executesql配合参数
预编译语句不是万能的,动态表名/列名必须白名单校验
参数化查询只解决“值”的问题,解决不了“结构”问题。比如 SELECT * FROM ? 或 ORDER BY ? 在绝大多数驱动里会直接报错——因为问号只能代入字面量,不能代入标识符。
这时候常见错误是写个 if table_name in ['users', 'orders']: 就放行,但忘了大小写、下划线、甚至 Unicode 零宽字符绕过。更稳妥的做法是:用固定映射表转换,而不是简单判断字符串是否在列表里。
- 例如把前端传来的
sort=user_name映射为内部字段"name",再拼进 SQL;而不是直接拼"ORDER BY " + user_input - PostgreSQL 中用
quote_ident()、MySQL 中用反引号包裹 + 正则校验(^[a-zA-Z_][a-zA-Z0-9_]*$),比单纯白名单多一层兜底 - 动态 IN 子句(如
WHERE id IN (1,2,3))不能只用一个参数占位符;得根据输入长度生成对应数量的?,再逐个绑定
渗透测试时重点盯住三个“非典型入口”
常规测试总盯着登录框和搜索框,但真实漏洞往往藏在更隐蔽的位置:ORDER BY 参数、分页的 limit/offset、以及 API 中的 include 字段(用于控制返回关联表)。
这些地方常被开发认为“只是数值或枚举”,结果却允许传入 id ASC, (SELECT password FROM users LIMIT 1) 这种 payload。测试时别只扫 ' OR 1=1--,要试 1 ORDER BY 1,2,3,4# 看报错信息,再试 1 UNION SELECT @@version-- 看是否回显。
-
limit参数若未转成整数类型,可能被注入为1, (SELECT ...)(MySQL 特有语法) - GraphQL 接口若用字符串拼接生成 SQL,
orderBy: "name ASC; DROP TABLE users;"可能直接执行 - 日志系统里记录了原始 SQL 报错信息?那
' AND SLEEP(5)--这类盲注行为会留下时间差痕迹,容易被漏掉
定期测试不能只跑工具,得模拟真实上下文
用 sqlmap 扫一遍返回“无注入点”,不代表真的安全。它默认不测 header、cookie、multipart body,也不懂你的业务逻辑约束(比如某字段只允许两位数字,但 sqlmap 仍会发长字符串试探)。
真正有效的渗透,是先读代码或文档,找出所有接受用户输入并拼入 SQL 的位置,再手工构造符合业务格式的 payload。比如手机号字段,就试 138' AND 1=1--,而不是盲目发 ' OR 'a'='a。
- 测试前确认数据库实际版本(
SELECT VERSION()),不同版本支持的函数、报错方式、堆叠注入能力都不同 - Web 应用若用了连接池,
; DROP TABLE类堆叠语句可能被中间件拦截,但UNION SELECT依然有效 - 生产环境禁止开启详细错误回显,但测试环境必须打开——否则连最基础的报错注入都发现不了
最麻烦的是那些“看似没拼接、实则间接拼接”的场景,比如配置中心里存了一段 SQL 模板,运行时用 str.format() 填充变量。这种链路一深,人眼很难覆盖全,得靠 AST 扫描或运行时 hook 检测 SQL 构造行为。










