从 SQL 注入案例看代码审计中的危险写法

SQL 注入(SQL Injection)长期占据 OWASP Top 10 的显著位置,尽管其原理早已被广泛讨论,但在实际代码审计中,危险写法依然层出不穷。本文从几个真实案例出发,剖析代码审计中常见的 SQL 注入危险模式,并顺带讨论 XSS 防护与服务器加固的相关实践。

一、案例:一个被忽视的 ORDER BY 注入

某 CMS 系统的文章列表接口接收 sort 参数,用于指定排序字段。开发者对 idkeyword 等参数都做了参数化处理,唯独 sort 字段采用了拼接:

$sort = $_GET['sort'];
$sql = "SELECT * FROM articles ORDER BY $sort DESC";

开发者认为 ORDER BY 后面只能是字段名,攻击者无法注入。但实际上,攻击者可以传入:

?sort=(CASE WHEN (SELECT SUBSTRING(password,1,1) FROM users LIMIT 1)='a' THEN id ELSE title END)

通过布尔盲注逐字符提取管理员密码。这类注入之所以危险,是因为它绕过了开发者对“参数化查询只适用于值”的片面理解——任何进入 SQL 语句结构的部分,包括字段名、排序方向、表名,都不能直接拼接用户输入

正确的做法是使用白名单映射:

$allowed = ['id', 'title', 'created_at'];
$sort = in_array($_GET['sort'], $allowed) ? $_GET['sort'] : 'id';
$sql = "SELECT * FROM articles ORDER BY $sort DESC";

二、代码审计中的五类危险写法

1. 字符串拼接构造 SQL

这是最经典的写法,也是审计中最容易发现的:

cursor.execute("SELECT * FROM users WHERE name = '%s'" % username)

危险点在于用户输入直接进入 SQL 语句。审计时应全局搜索 executequeryraw 等关键字附近的字符串拼接操作。

2. LIKE 语句中的参数化误用

参数化查询本身没有问题,但 LIKE 通配符的处理容易出错:

sql = "SELECT * FROM users WHERE name LIKE '%?%'"
cursor.execute(sql, (keyword,))

这里的 ? 被引号包裹,参数化失效。正确写法是:

sql = "SELECT * FROM users WHERE name LIKE ?"
cursor.execute(sql, ('%' + keyword + '%',))

3. IN 子句的动态拼接

当需要根据数组长度动态生成占位符时,开发者容易退回拼接:

ids = ",".join(request.args.getlist('id'))
sql = f"SELECT * FROM users WHERE id IN ({ids})"

安全写法是生成与数组等长的占位符:

placeholders = ",".join(["%s"] * len(ids))
sql = f"SELECT * FROM users WHERE id IN ({placeholders})"
cursor.execute(sql, ids)

4. 框架的“安全假象”

ORM 框架并非万能。Django 的 extra()raw(),SQLAlchemy 的 text() 拼接,MyBatis 的 ${} 而非 #{},都是审计重点。例如 MyBatis 中:

<select id="getUser">
  SELECT * FROM users WHERE name = '${name}'
</select>

${} 是直接替换,#{} 才是预编译占位。审计 MyBatis 映射文件时,搜索 ${ 是基本操作。

5. 存储过程内的动态 SQL

存储过程中使用 EXECEXECUTE IMMEDIATE 拼接参数,同样存在注入风险。审计时需关注存储过程内部的动态语句构造。

三、MySQL 防止 SQL 注入的几种写法

结合实际开发,MySQL 环境下推荐以下防护层次:

第一层:参数化查询(首选)

$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);

第二层:白名单校验

对于无法参数化的部分(表名、字段名、排序方向),使用严格白名单:

$order = $_GET['order'] === 'asc' ? 'ASC' : 'DESC';

第三层:最小权限原则

为 Web 应用分配独立的数据库账号,仅授予必要的 SELECT、INSERT、UPDATE、DELETE 权限,禁用 FILE、DROP 等高危权限。即使注入成功,攻击者也无法读取文件或提权。

第四层:输入类型强制转换

对于整数型参数,强制转换是最简单有效的防御:

$id = (int)$_GET['id'];

四、XSS 的常见方式与前端防御

SQL 注入与 XSS 常在同一系统中并存,两者都源于“未经验证的用户输入”。XSS 常见方式包括:

  • 反射型:恶意脚本通过 URL 参数反射到页面,如搜索关键词未转义直接输出。
  • 存储型:恶意内容存入数据库,在文章评论、用户昵称等位置持久化输出。
  • DOM 型:前端 JavaScript 直接使用 location.hashinnerHTML 等操作 DOM,不经过服务端。

前端防御的核心原则是输出编码上下文感知

  1. 使用 textContent 而非 innerHTML 插入用户内容。
  2. 对 HTML 属性、URL、JavaScript 等不同上下文采用对应的编码方式。
  3. 设置 Content-Security-Policy(CSP)头,限制脚本来源。
  4. 对富文本内容使用 DOMPurify 等库进行白名单过滤。

需要强调的是,前端防御不能替代服务端过滤。攻击者可以绕过前端直接构造请求,服务端必须对输出进行编码。

五、Linux 服务器安全加固清单

代码层面的防护需要服务器层面的配合。以下是一份精简的加固清单:

  1. 系统更新:定期执行 apt upgradeyum update,修补内核与组件漏洞。
  2. 最小化服务:关闭不必要的端口与服务,使用 ss -tlnp 审查监听端口。
  3. SSH 加固:禁用 root 远程登录,改用密钥认证,修改默认端口,配置 fail2ban
  4. 防火墙:使用 iptables 或 ufw 仅放行必要端口。
  5. 文件权限:Web 目录设为 www-data 只读,上传目录禁止执行脚本。
  6. 日志审计:开启 auditd,集中收集 Nginx、MySQL、应用日志。
  7. 数据库隔离:MySQL 仅监听 127.0.0.1,禁止外网直连。
  8. 备份与恢复:定期备份并验证恢复流程,防止勒索软件。

结语

SQL 注入的案例反复说明一个道理:安全问题的根源往往不是技术难度,而是开发习惯。代码审计的价值在于从大量代码中识别出那些“看起来没问题”的危险写法。参数化查询、白名单校验、最小权限、输出编码——这些原则并不复杂,难的是在每一行代码中坚持执行。将安全左移,在编码阶段就建立规范,远比在上线后修补漏洞更有效。

未经允许不得转载:任鹏个人博客 » 从 SQL 注入案例看代码审计中的危险写法

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏