SQL 注入(SQL Injection)长期位居 OWASP Top 10 之首,是 Web 应用中最危险、最普遍的漏洞类型之一。对于安全审计人员而言,快速识别代码中的危险函数和易受攻击的写法,是发现 SQL 注入漏洞的核心能力。本文将从代码审计的视角出发,系统梳理常见危险函数、典型漏洞模式,并给出切实可行的修复方案。
一、SQL 注入的本质
SQL 注入的本质是:用户可控的输入被拼接进 SQL 语句中,并被数据库当作代码执行。这意味着两个条件同时成立时,漏洞即存在:
- 数据来源可控(如 GET/POST 参数、Cookie、HTTP Header);
- 数据流向 SQL 执行函数,且未做充分的参数化处理。
代码审计的核心工作,就是追踪“污点数据”从入口到执行点的完整链路。
二、常见危险函数与写法
1. 字符串拼接 + 查询执行
这是最经典、最直观的漏洞模式。在 Java、PHP、Python、C# 等语言中均广泛存在。
PHP 示例:
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$result = mysqli_query($conn, $sql);
Java 示例:
String id = request.getParameter("id");
String sql = "SELECT * FROM users WHERE id = " + id;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
Python 示例:
id = request.args.get('id')
cursor.execute("SELECT * FROM users WHERE id = %s" % id)
以上代码均未对输入做参数化处理,攻击者只需传入 1 OR 1=1 或 1; DROP TABLE users-- 即可实现注入。
2. 危险函数清单
不同语言和框架中,需要重点关注的执行函数包括:
- PHP:
mysql_query、mysqli_query、PDO::query、PDO::exec(当拼接字符串时) - Java:
Statement.execute、Statement.executeQuery、Statement.executeUpdate、prepareStatement中仍拼接字符串的写法 - Python:
cursor.execute(配合%、format、+拼接)、pymysql、sqlite3的原始执行 - C#/.NET:
SqlCommand配合字符串拼接、ExecuteNonQuery、ExecuteReader - Node.js:
mysql.query、sequelize.query(原始 SQL 模式)
3. 容易被忽视的注入点
除常见的 WHERE 条件外,以下位置同样容易产生注入:
- ORDER BY / GROUP BY:无法使用预编译占位符,需白名单校验;
- LIMIT / OFFSET:部分数据库驱动不支持参数化;
- 表名、列名:动态拼接时极易注入;
- IN 子句:拼接
IN (1,2,3)时若未做类型转换; - LIKE 语句:
LIKE '%$keyword%'中的通配符拼接。
4. 二次注入
二次注入(Second-Order Injection)更为隐蔽:恶意数据先被“安全地”存入数据库,之后在另一处被取出并拼接进 SQL 语句执行。审计时需关注数据的完整生命周期,而非单点检查。
三、修复方案
1. 首选:参数化查询(预编译)
参数化查询是防御 SQL 注入最有效的手段。它将 SQL 语句结构与数据分离,使数据永远不会被解析为代码。
Java 正确写法:
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, Integer.parseInt(id));
ResultSet rs = pstmt.executeQuery();
PHP 正确写法(PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
Python 正确写法:
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
注意:Python 中必须使用参数元组,而非 % 字符串格式化。
2. 输入验证与类型强制
对于无法参数化的场景(如 ORDER BY 列名),采用白名单校验:
Set<String> allowed = Set.of("id", "name", "created_at");
if (!allowed.contains(sortField)) {
throw new IllegalArgumentException("Invalid sort field");
}
String sql = "SELECT * FROM users ORDER BY " + sortField;
数字型参数应强制转换为整型:
$id = intval($_GET['id']);
3. 最小权限原则
数据库账户应遵循最小权限原则:Web 应用使用的账户不应具备 DROP、FILE、GRANT 等高危权限,从而在注入发生时限制损害范围。
4. 统一数据访问层
在项目架构中封装统一的 DAO 层,禁止业务代码直接拼接 SQL。通过代码规范和静态扫描工具(如 SonarQube、Fortify、CodeQL)在 CI 阶段拦截危险写法。
5. ORM 框架的正确使用
MyBatis、Hibernate、SQLAlchemy 等 ORM 框架本身具备防注入能力,但错误使用仍会引入漏洞:
- MyBatis 中
${}是字符串拼接,#{}才是预编译占位符,应优先使用#{}; - Hibernate 的 HQL 拼接、
createSQLQuery原始 SQL 需谨慎; - SQLAlchemy 的
text()中拼接变量同样危险。
四、审计思路小结
进行 SQL 注入代码审计时,可遵循以下流程:
- 定位入口:找出所有接收外部输入的位置(Request、Cookie、Header);
- 追踪污点:沿数据流追踪变量传递路径;
- 识别执行点:定位所有 SQL 执行函数;
- 判断过滤:检查是否使用参数化、白名单或转义;
- 验证利用:构造 PoC 确认漏洞可被触发。
五、结语
SQL 注入虽然“古老”,但因开发习惯、历史代码和框架误用,至今仍是 Web 安全的主要威胁之一。防御的核心原则只有一条:永远不要相信用户输入,永远不要拼接 SQL。参数化查询配合最小权限、白名单校验和统一数据访问层,可以构建起多层次的纵深防御体系。对于安全从业者而言,掌握危险函数的识别能力和污点追踪的审计思路,是高效发现并修复 SQL 注入漏洞的关键。
未经允许不得转载:任鹏个人博客 » SQL 注入漏洞代码审计:常见危险函数与修复方案


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