在防止SQL注入的代码审计工作中,核心动作就是找到项目里所有直接拼接SQL语句的危险函数调用,然后逐一替换为参数化查询或预编译语句。具体来说,你需要重点搜索的危险函数包括:PHP中的mysql_query、mysqli_query、pg_query、mssql_query、oci_execute等直接执行SQL的函数,以及sprintf、str_replace、拼接运算符(.)等用于构造SQL语句的字符串操作函数。替换方案很明确——把所有动态拼接SQL的地方改成PDO预处理(prepare+bindParam/execute)或者mysqli的prepare+bind_param模式,从根本上杜绝用户输入直接进入SQL语句结构。
做代码审计不是随便扫一遍代码就完事,你得有系统的方法论。下面我从危险函数识别、搜索策略、替换方案、审计检查清单四个维度,把这件事讲透。
一、SQL注入的本质与危险函数的关系SQL注入之所以能发生,根本原因是用户可控的数据被当作SQL语句的一部分直接拼接进去执行了。比如一个登录表单,用户输入用户名admin' OR '1'='1,如果代码直接把这个字符串拼进WHERE子句,数据库就会执行一条完全不同的查询逻辑。危险函数就是那些"不做任何过滤就把字符串当SQL执行"的函数。你在审计时,找到这些函数的调用点,就等于找到了潜在的注入入口。
需要特别注意的是,危险不仅仅来自直接执行SQL的函数。有些函数虽然不直接执行SQL,但它们负责构造SQL语句,比如用sprintf拼接、用字符串连接符(PHP的.、Java的+、Python的%或f-string)把变量塞进SQL模板里。这些地方同样是高风险点,审计时必须一并纳入搜索范围。
二、各语言中需要重点搜索的危险函数清单不同编程语言的危险函数不一样,下面按主流语言分别列出:
PHP语言:
// 直接执行SQL的危险函数
mysql_query() // 已废弃但老项目大量存在
mysqli_query() // 最常见的危险函数
pg_query() // PostgreSQL
mssql_query() // SQL Server
oci_execute() // Oracle
PDO::query() // PDO中直接执行的方法
// 构造SQL的危险操作
sprintf("SELECT * FROM users WHERE id=%d", $id)
"SELECT * FROM users WHERE name='" . $name . "'"
str_replace() 配合SQL拼接
Java语言:
// 危险的SQL执行方式
Statement.execute()
Statement.executeQuery()
Statement.executeUpdate()
// 危险的字符串拼接
String sql = "SELECT * FROM users WHERE id=" + userId;
String.format("SELECT * FROM users WHERE name='%s'", name)
Python语言:
# 危险的执行方式
cursor.execute(sql) # 直接传拼接好的字符串
cursor.executemany(sql, params) # 如果sql是拼接的也危险
# 危险的字符串构造
sql = "SELECT * FROM users WHERE id=%s" % user_id
sql = f"SELECT * FROM users WHERE name='{name}'"
C#/.NET语言:
// 危险方式 SqlCommand.ExecuteReader() SqlCommand.ExecuteNonQuery() // 配合字符串拼接构造SQL string sql = "SELECT * FROM Users WHERE Id=" + userId;
审计时,你需要根据项目使用的技术栈,把对应语言的危险函数全部列入搜索清单,一个都不能漏。
三、高效搜索危险函数的实操策略手动一行行看代码效率太低,必须借助工具和正则表达式。以下是几种实用的搜索方法:
1. 使用IDE全局搜索功能
在VS Code、PhpStorm、IntelliJ IDEA等IDE中,使用全局搜索(Ctrl+Shift+F),输入函数名如mysqli_query、execute等,可以快速定位所有调用点。建议按函数逐个搜索,记录每个文件和行号。
2. 正则表达式批量匹配
对于构造SQL的拼接模式,可以用正则表达式。比如在PHP项目中搜索字符串拼接构造SQL的模式:
// 匹配类似 "SELECT ... '" . $var . "'" 的模式 /["']\s*\.\s*\$[a-zA-Z_][a-zA-Z0-9_]*\s*\.\s*["']/ // 匹配sprintf构造SQL /sprintf\s*\(\s*["'].*?(SELECT|INSERT|UPDATE|DELETE).*?["']\s*,/i
3. 静态代码分析工具
使用专业的SAST工具可以大幅提升效率。比如PHP可以用RIPS、SonarQube、PHPStan;Java可以用SpotBugs、FindSecurityBugs;Python可以用Bandit。这些工具能自动识别SQL注入风险点,给出具体的文件位置和代码行。但工具不是万能的,最终还是需要人工复核,因为有些复杂的业务逻辑工具可能判断不准。
4. 建立审计台账
每找到一个危险函数调用点,都要记录:文件路径、行号、函数名、是否有用户输入参与、当前是否有过滤措施。这样后续替换和验证时才有依据,不会遗漏。
四、危险函数的替换方案与代码示例找到危险函数只是第一步,关键是怎么替换。核心原则只有一条:永远不要把用户输入直接拼接进SQL语句,必须使用参数化查询。
PHP + MySQLi 替换示例:
// 危险写法
$sql = "SELECT * FROM users WHERE username='" . $_POST['username'] . "' AND password='" . $_POST['password'] . "'";
$result = mysqli_query($conn, $sql);
// 安全写法 - 使用预处理语句
$stmt = $conn->prepare("SELECT * FROM users WHERE username=? AND password=?");
$stmt->bind_param("ss", $_POST['username'], $_POST['password']);
$stmt->execute();
$result = $stmt->get_result();
PHP + PDO 替换示例:
// 危险写法
$sql = sprintf("SELECT * FROM orders WHERE user_id=%d", $_GET['id']);
$stmt = $pdo->query($sql);
// 安全写法
$stmt = $pdo->prepare("SELECT * FROM orders WHERE user_id=:id");
$stmt->bindParam(':id', $_GET['id'], PDO::PARAM_INT);
$stmt->execute();
Java 替换示例:
// 危险写法
String sql = "SELECT * FROM users WHERE id=" + request.getParameter("id");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 安全写法
String sql = "SELECT * FROM users WHERE id=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, Integer.parseInt(request.getParameter("id")));
ResultSet rs = pstmt.executeQuery();
Python 替换示例:
# 危险写法 sql = "SELECT * FROM users WHERE name='%s'" % user_input cursor.execute(sql) # 安全写法 sql = "SELECT * FROM users WHERE name=%s" cursor.execute(sql, (user_input,))
替换时还有几个细节要注意:第一,如果原来的SQL里有动态表名、列名等不能用参数绑定的部分,必须用白名单校验,绝对不能直接拼接用户输入;第二,bind_param的类型参数要和实际数据类型匹配,类型错误会导致静默失败;第三,对于LIKE查询,通配符要在绑定值里拼接,不要在SQL语句里拼。
五、代码审计中容易被忽略的高危场景很多审计人员只盯着登录、注册这些明显的输入点,但以下场景同样危险且容易被忽略:
1. HTTP Header注入:Cookie、User-Agent、Referer等HTTP头的值如果被存入数据库或用于构造查询,同样存在注入风险。审计时要检查所有从请求中获取数据的地方。
2. 二次注入:数据第一次存入时做了过滤,但从数据库读出来再次拼接SQL时又没做处理。这种情况在数据迁移、日志记录、报表生成等场景很常见。
3. ORM框架的原生查询:很多人以为用了ORM就安全了,但如果在ORM里使用原生SQL(raw query、native query),风险和手写SQL一样高。比如Laravel的DB::raw()、Hibernate的createNativeQuery(),都需要检查参数是否绑定。
4. 存储过程调用:如果存储过程内部使用了动态SQL拼接,即使外部调用用了参数化,注入风险依然存在。审计时需要检查存储过程的实现代码。
5. 文件上传相关:文件名、文件内容如果被存入数据库或用于构造查询,也是注入点。特别是CSV导入、Excel解析等功能,数据来源不可控。
六、审计完成后的验证与持续防护替换完所有危险函数后,不能就此结束。你需要做三件事:第一,用SQLMap等工具对修改后的接口做自动化渗透测试,验证是否还有残留的注入点;第二,做代码diff对比,确认每一处修改都正确无误,没有引入新的逻辑错误;第三,建立代码规范,在团队的编码标准中明确禁止SQL拼接,强制要求使用参数化查询,从源头防止新代码引入注入风险。
从长远来看,防止SQL注入不能只靠事后审计。建议在CI/CD流水线中集成静态分析工具,每次代码提交自动扫描危险函数调用。同时定期做安全培训,让开发人员理解参数化查询的原理和写法。技术手段和人员意识双管齐下,才能真正把SQL注入的风险降到最低。
总结一下,防止SQL注入的代码审计核心就是三步:全面搜索危险函数和拼接模式、逐一替换为参数化查询、验证加持续防护。这件事没有捷径,但只要方法对、执行到位,绝大多数SQL注入漏洞都能被有效发现和修复。
