SQL 注入(SQL Injection)长期占据 OWASP Top 10 的显著位置,尽管其原理早已被广泛讨论,但在实际代码审计中,危险写法依然层出不穷。本文从几个真实案例出发,剖析代码审计中常见的 SQL 注入危险模式,并顺带讨论 XSS 防护与服务器加固的相关实践。
一、案例:一个被忽视的 ORDER BY 注入
某 CMS 系统的文章列表接口接收 sort 参数,用于指定排序字段。开发者对 id、keyword 等参数都做了参数化处理,唯独 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 语句。审计时应全局搜索 execute、query、raw 等关键字附近的字符串拼接操作。
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
存储过程中使用 EXEC、EXECUTE 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.hash、innerHTML等操作 DOM,不经过服务端。
前端防御的核心原则是输出编码与上下文感知:
- 使用
textContent而非innerHTML插入用户内容。 - 对 HTML 属性、URL、JavaScript 等不同上下文采用对应的编码方式。
- 设置 Content-Security-Policy(CSP)头,限制脚本来源。
- 对富文本内容使用 DOMPurify 等库进行白名单过滤。
需要强调的是,前端防御不能替代服务端过滤。攻击者可以绕过前端直接构造请求,服务端必须对输出进行编码。
五、Linux 服务器安全加固清单
代码层面的防护需要服务器层面的配合。以下是一份精简的加固清单:
- 系统更新:定期执行
apt upgrade或yum update,修补内核与组件漏洞。 - 最小化服务:关闭不必要的端口与服务,使用
ss -tlnp审查监听端口。 - SSH 加固:禁用 root 远程登录,改用密钥认证,修改默认端口,配置
fail2ban。 - 防火墙:使用 iptables 或 ufw 仅放行必要端口。
- 文件权限:Web 目录设为
www-data只读,上传目录禁止执行脚本。 - 日志审计:开启
auditd,集中收集 Nginx、MySQL、应用日志。 - 数据库隔离:MySQL 仅监听
127.0.0.1,禁止外网直连。 - 备份与恢复:定期备份并验证恢复流程,防止勒索软件。
结语
SQL 注入的案例反复说明一个道理:安全问题的根源往往不是技术难度,而是开发习惯。代码审计的价值在于从大量代码中识别出那些“看起来没问题”的危险写法。参数化查询、白名单校验、最小权限、输出编码——这些原则并不复杂,难的是在每一行代码中坚持执行。将安全左移,在编码阶段就建立规范,远比在上线后修补漏洞更有效。
未经允许不得转载:任鹏个人博客 » 从 SQL 注入案例看代码审计中的危险写法


朋友圈点赞图在线生成源码