SQL 注入(SQLi)深度解析【原理、检测、风险、防护措施】

SQL 注入(SQLi)深度解析【原理、检测、风险、防护措施】

一、什么是 SQL 注入(概念)

SQL 注入是指攻击者把恶意 SQL 代码注入到应用的输入里,应用在拼接或执行这些输入构造的 SQL 时,导致数据库执行非预期命令(数据泄露、篡改、权限提升、远程命令执行等)。根源是不安全的用户输入被直接拼接到 SQL 语句,未使用参数化语句或未正确转义。

二、注入类型与原理

基于错误(Error‑based)

利用数据库错误消息泄露结构信息或敏感数据。例如通过 UNION 或构造语法错误使数据库返回可被解析的内容。

基于联合(Union‑based)

使用 UNION SELECT 合并攻击者控制的查询结果到原查询的结果集中,从而读取表数据。

布尔盲注(Boolean‑based Blind)

利用不同布尔条件改变页面返回(或返回状态)来逐位推断数据,例如 AND (SUBSTR((SELECT password FROM users WHERE id=1),1,1)='a')。

时间盲注(Time‑based Blind)

利用延迟函数(如 SLEEP()、pg_sleep())判断条件是否成立(用于不能从错误/返回区别的场景)。

基于堆叠查询(Stacked Queries)(部分 DB 如 MSSQL/MySQL 的某些配置)

在单个请求中执行多条独立 SQL(例如 '; DROP TABLE users; --)。

Out‑of‑Band (OOB)

利用数据库或环境与外部服务器通信(DNS/HTTP)来回传数据(当直接通道受限时非常有用)。

二次注入(Second‑order)

恶意数据先存入数据库,随后在不同的上下文或查询中被回显并触发注入(常被静态检测漏掉)。

三、关键危险点(为什么会产生)

直接把用户输入拼接到 SQL 字符串中。在不可信上下文中动态构建表名/列名/排序字段而不做校验。数据库错误/调试信息在生产中泄露(信息泄露助攻)。过高的数据库账户权限(例如具有 DROP/ADMIN 权限)。缺乏最小权限、审计和监控。

四、易受攻击的源码示例(含修复)

说明:示例同时给出“易受攻击代码”与“推荐修复”,方便审计和直接替换。

1) Java(JDBC) — 易受攻击

// vulnerable

String q = "SELECT id, name FROM users WHERE email = '" + email + "' AND status = '" + status + "'";

Statement s = conn.createStatement();

ResultSet rs = s.executeQuery(q);

问题:email、status 直接拼接。攻击者可注入 ' OR '1'='1 等。

修复(PreparedStatement)

String q = "SELECT id, name FROM users WHERE email = ? AND status = ?";

PreparedStatement ps = conn.prepareStatement(q);

ps.setString(1, email);

ps.setString(2, status);

ResultSet rs = ps.executeQuery();

要点:参数化查询(PreparedStatement)把输入作为值传递,避免语法注入。

2) PHP(mysqli) — 易受攻击

// vulnerable

$q = "SELECT * FROM products WHERE name LIKE '%" . $_GET['q'] . "%'";

$res = $mysqli->query($q);

修复(绑定参数):

$stmt = $mysqli->prepare("SELECT * FROM products WHERE name LIKE CONCAT('%', ?, '%')");

$stmt->bind_param("s", $_GET['q']);

$stmt->execute();

3) Python(psycopg2 / PostgreSQL) — 易受攻击

# vulnerable

cur.execute("SELECT * FROM orders WHERE id = %s" % order_id)

修复:

cur.execute("SELECT * FROM orders WHERE id = %s", (order_id,))

要点:使用 DB API 的参数化接口,不要手动 format 字符串。不同驱动使用不同占位符(%s, ?, :1 等)——要用驱动的参数方式。

4) ORM(如 Hibernate / JPA / ActiveRecord)常见陷阱

易受攻击:

// JPA with string concatenation

String q = "FROM User u WHERE u.name = '" + name + "'";

entityManager.createQuery(q).getResultList();

修复(查询绑定):

Query q = entityManager.createQuery("FROM User u WHERE u.name = :name");

q.setParameter("name", name);

注意:即使使用 ORM,也可能因为使用 nativeQuery、动态拼表名、ORDER BY 字段拼接等导致注入。对列名、表名、排序字段必须做白名单校验,不能直接把用户输入当成结构的一部分。

5) Stored Procedures(存储过程)

存储过程本身不是万能防护:若在存储过程内部仍通过字符串拼接并 EXEC(),仍会注入。正确做法是使用参数化调用与内部参数化 SQL。

五、常见攻击 Payload(用于检测/测试,防御目的)

(用于安全测试,勿用于非法入侵)

基本条件绕过: ' OR '1'='1终止并注入: '; DROP TABLE users; -- (注意很多 DB+驱动会禁用多语句)UNION 读取列: '+ UNION SELECT username, password FROM users -- 布尔盲注示例: 1' AND SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a' -- 时间盲注(MySQL): ' OR SLEEP(5) --

这些 payload 在现代防护环境下可能被 WAF 拦截或触发 IDS。用于渗透测试时要在授权环境进行。

六、检测与测试方法(审计/渗透/自动化)

静态代码审计(SAST)

搜索易危险 API:createStatement, executeQuery(String), rawQuery, execSQL, mysqli->query, PDO->query, odbc_exec, execute 带字符串的情况。搜索字符串拼接、String.format、模板化 SQL、+ 拼接中的用户输入。检查 ORM 的 nativeQuery、fromSqlRaw 等危险方法。

动态测试(DAST / 手工)

在输入点注入布尔/语法错误/时间延迟 payload,观察响应差异/延迟/错误信息。使用 sqlmap(在授权测试)自动化识别盲注、UNION、时间盲注等。模拟高权限用户/不同角色测试二次注入。

日志/监控检测

识别异常 SQL 错误、语法异常、频繁的延迟(time‑based),或来自同一 IP 的大量带有 SQL 特征的请求。配置 DB 审计(如 MySQL 连接日志、Oracle audit)来追踪可疑查询。

模糊测试 & 代码覆盖率

针对拼接发生路径做模糊输入测试,尤其是动态 SQL、搜索、过滤、排序、分页等入口。

七、防护策略(按优先级与细节实现)

1. 最重要:参数化查询 / Prepared Statements / ORM 参数绑定

所有 SQL 语句的值部分必须使用参数绑定,不要做字符串拼接。在支持的语言与驱动中使用预编译语句(JDBC PreparedStatement、PDO prepared statements、psycopg2 参数化等)。

2. 对结构性输入使用白名单验证

用户不能控制 SQL 的结构(列名、表名、ORDER BY 字段、LIMIT 等)。若必须支持,须对允许的值做严格白名单(枚举)校验。示例:if (!allowedSorts.contains(userSort)) throw ...;

3. 最小权限数据库账户

每个应用只使用权限最小的 DB 用户(只 GRANT 必要的 SELECT/INSERT/UPDATE/DELETE)。拒绝 DROP/ALTER 权限给普通应用账户,防止被注入后破坏。

4. 错误信息与异常处理

生产环境关闭详细 SQL 错误输出,避免泄露表结构/列名。记录详细日志并发送到安全审计系统,但不要将详细错误暴露给用户。

5. 输入校验(白名单优先)与类型检查

对数字、UUID、日期等强制进行类型验证(避免将用户输入当作字符串直接拼接)。长度限制、字符集限制可以作为额外防线,但不能替代参数化。

6. WAF / 入侵检测(作为补充,而非主防线)

部署 WAF 能拦截常见攻击模式(UNION、注入关键词、延迟请求),但不要把 WAF 当作唯一防护。

7. 安全配置

禁用不必要的 DB 功能(如允许多语句的选项、允许外部程序执行等)如果不需要就关闭。更新 DB 与驱动,利用现代驱动的安全特性。

8. 审计与监控

DB 审计日志、慢查询日志、异常 SQL 报警。将异常模式(如包含 ' OR '1'='1')建立告警规则。

9. 定期安全测试

SAST 于 CI 中自动扫描,DAST 定期运行(并在变更后手动复测)。授权渗透测试(包含 sqlmap 扫描、手工盲注测试、二次注入场景)。

10. 防止二次注入

当应用先存储数据再在另一处做 SQL 拼接时,必须对回显/二次使用的场景特别注意(例如,一个字段先保存,再在动态 SQL 中作为列名使用)。

八、风险评估(影响面与严重性)

高风险:能够读取敏感表(用户、密码/哈希、支付信息)、能够写入/修改关键数据(更改权限、写入管理员账号)、能够执行 OS 命令或写入文件(如通过 xp_cmdshell 等在 MSSQL)——可能导致全面失陷。中风险:能枚举内部表结构或大量数据但无法直接变更关键业务数据。低风险:仅影响非敏感字段的查询或仅导致页面异常。

严重性评估应结合:受影响的用户数、被窃取数据的敏感性、是否可导致持久后门或权限提升。

九、工程化 Checklist(代码审计/修复优先级)

优先替换所有拼接 SQL(尤其用户输入参与的)。检查所有 exec/query/rawQuery/nativeQuery 的调用:是否包含用户输入?若包含,使用参数化或白名单。对所有动态结构(表名、列、order by)做白名单验证。限制 DB 账户权限,确保无法进行 DROP/ALTER/EXEC 等高危操作。关闭生产详细错误输出,启用审计日志并集中告警。将 SAST 规则加入 CI(关键关键字/模式自动告警)。对曾暴露的表/列进行凭证重置(若怀疑泄露),并做应急响应(隔离、审计、回滚/修补)。定期(至少季度)进行授权渗透测试与代码审计。

十、常见误区与陷阱

“只做输入过滤就够” —— 错误。输入过滤可以减少攻击面,但不能替代参数化。“ORM 自动安全” —— ORM 不会自动避免所有注入(特别是使用原生 SQL、拼接查询、分页排序等场景)。“WAF 就万无一失” —— WAF 是补充,不能替代安全编码与最小权限。“只检查 Web 输入” —— 注入向量还包含批处理、内部系统任务、第三方集成数据、日志/上传文件元数据等。

十一、示例应急响应步骤(如果发现注入)

迅速下线或临时修复:对受影响端点临时做输出转义或限制访问(若可行)。查明范围:哪些 API/页面受影响,是否有已被泄露的数据。审计日志:导出数据库访问日志、错误日志,定位可疑查询与时间线。更改凭证:若怀疑凭证被泄露(用户/管理员密码、API keys),按策略重置。修复根本原因:用参数化查询替换拼接逻辑、修复二次注入点。回溯调查:确认是否存在后门/持久化修改,清理并恢复。通报并加固:内部复盘、合规通报(如需),持续监控。

十二、快速参考(实用命令/关键字 - 审计时 grep)

常用待搜索关键字(代码中):

createStatement|executeQuery\(|query\(|execSQL\(|rawQuery|nativeQuery|format\(|String.format\(|\+ .*sql|\%s.*execute

前端/后端要着重找拼接字符串、使用 exec、Runtime.exec 相关的组合用法。