老旧代码系统里的SQL注入问题,本质上不是“漏洞没补”,而是整个数据访问层长期处于无参数化、无输入约束、无统一入口的状态。很多系统运行超过五年甚至十年,底层拼接SQL的写法遍布业务代码,单纯靠打补丁或加防火墙无法根治。要真正完成安全升级,需要从定位注入点、改造执行链路、建立输入校验、上线后监控四个层面同时推进。
先定位所有SQL执行入口
老旧系统最常见的特征是数据库操作分散在几十甚至上百个文件里,有的用原生mysql_query,有的用mysqli_query,还有的封装了一半又绕回去拼接字符串。升级第一步不是马上改代码,而是把所有执行SQL的地方找出来。可以借助代码扫描工具搜索关键词,例如mysql_query、mysqli_query、pg_query、sqlsrv_query、executeQuery、createStatement等。同时搜索字符串拼接特征,比如“select ”、“where ”、“and ”后面跟着变量名和点号连接符。
更隐蔽的是二次拼接,例如先把用户输入存入某个变量,再在另一个函数里拼进SQL。对于这类情况,需要追踪变量的来源,尤其是来自$_GET、$_POST、$_REQUEST、$_COOKIE、$_SERVER以及外部接口返回的数据。定位完成后建立一张清单,标明文件路径、函数名、SQL语句类型、输入来源、当前是否已经过滤。这张清单是后续改造的工作底稿,也能防止遗漏。
统一数据访问层,废除裸拼SQL
很多老旧系统没有统一的数据访问层,业务代码直接操作数据库连接。安全升级的核心动作是建立或强化一个统一的SQL执行入口,让所有数据库操作都必须经过这个入口。入口内部只允许参数化查询,不允许传入已经拼接好的SQL字符串。
以PHP为例,如果系统还在用旧的mysql扩展,应当先迁移到mysqli或PDO。迁移时不要简单替换函数名,而要强制使用预处理语句。下面是一段典型的改造前代码:
$id = $_GET['id']; $sql = "SELECT * FROM users WHERE id = " . $id; $result = mysql_query($sql);
改造后应当变成:
$id = $_GET['id'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetchAll();
如果业务代码中大量存在动态表名、动态字段名、动态排序字段,参数化查询无法直接绑定标识符。这类场景必须使用白名单机制。例如排序字段只允许id、username、create_time,排序方向只允许ASC和DESC。任何不在白名单内的输入直接拒绝或回退到默认值。不要试图通过正则过滤来“清洗”表名和字段名,因为标识符规则复杂,正则遗漏概率高。
输入校验要前置,类型约束要明确
参数化查询解决的是SQL语法层面的注入,但业务逻辑安全还需要输入校验。老旧系统往往只在最外层做一次htmlspecialchars或addslashes,这种思路本身就是错的。SQL注入防护不应该依赖转义函数,而应该依赖类型约束和格式校验。
对于整数型参数,使用intval或filter_var强制转换。对于字符串参数,先定义长度上限和字符集范围。例如用户名只允许字母、数字和下划线,长度不超过32位;手机号只允许11位数字;邮箱必须符合基本格式。校验失败直接返回错误,不进入后续SQL执行流程。这样即使某些参数化查询因为历史原因没有覆盖到,前置校验也能大幅降低注入风险。
需要特别注意编码问题。老旧系统可能使用GBK、GB2312等宽字节字符集,如果数据库连接字符集与页面字符集不一致,历史上曾出现过宽字节注入绕过转义的情况。升级时应统一使用UTF-8字符集,并在数据库连接建立后立即执行set names utf8mb4或PDO的charset=utf8mb4参数。避免使用SET CHARACTER SET这种可能被二次解析的写法。
处理存储过程和动态SQL
部分老旧系统依赖存储过程,而存储过程内部如果使用EXEC或sp_executesql拼接字符串,同样存在注入风险。升级时不能只看应用层代码,还要审查数据库中的存储过程、视图和触发器。存储过程中的动态SQL必须改为参数化调用,例如在SQL Server中使用sp_executesql并传入参数定义,而不是直接EXEC字符串。
对于MySQL中的预处理语句,应用层已经参数化后,存储过程内部不应再次拼接。如果存储过程接收参数后又拼接到LIMIT、ORDER BY等位置,同样需要白名单控制。升级方案中应明确存储过程的输入参数类型、长度和允许范围,并在数据库层面增加约束。
替换或封装旧式数据库扩展
PHP 5.x时代大量使用的mysql扩展在PHP 7中已经移除,但仍有不少老旧系统运行在低版本环境中。升级方案必须包含运行环境升级计划。如果暂时无法升级PHP版本,可以先用mysqli或PDO封装一个兼容层,将原有mysql_query等函数替换为自定义函数,内部使用参数化查询。但这种方式只是过渡,最终目标仍然是升级到受支持的PHP版本并使用PDO。
Java老旧系统常见的问题是使用Statement而不是PreparedStatement,或者虽然使用了PreparedStatement,但通过字符串拼接构造SQL后再传入。例如:
String sql = "SELECT * FROM users WHERE name = '" + name + "'"; PreparedStatement ps = conn.prepareStatement(sql);
这种写法等于没有参数化。正确做法是SQL语句中只包含占位符,参数通过setString、setInt等方法传入。升级时需要逐个检查PreparedStatement的使用方式,确认SQL字符串中没有拼接外部输入。
修复ORM框架中的原生SQL和HQL注入
老旧系统可能使用了Hibernate、MyBatis等ORM框架,但框架本身并不能完全防止SQL注入,尤其是原生SQL和动态SQL片段。MyBatis中常见的注入风险是使用${}而不是#{}。${}是字符串替换,#{}才是参数绑定。升级方案中应全局搜索Mapper XML文件中的${},逐一评估是否可以改为#{}。对于确实需要动态表名或列名的场景,使用白名单校验后再拼接。
Hibernate中HQL注入同样存在,例如通过字符串拼接构造HQL查询。升级时应统一使用命名参数或位置参数,禁止直接拼接用户输入。对于Criteria API,也要注意不要将用户输入直接作为字段名或排序条件。
建立数据库账号最小权限原则
老旧系统的数据库账号往往拥有过高权限,有的直接使用root或sa连接。SQL注入一旦成功,攻击者可能直接读取系统表、执行系统命令或拖走整个数据库。安全升级必须包含权限收敛。应用连接数据库的账号只授予业务所需的SELECT、INSERT、UPDATE、DELETE权限,不授予DROP、ALTER、CREATE、FILE、EXECUTE等危险权限。对于需要执行存储过程的账号,只授予特定存储过程的执行权限,而不是整个数据库的执行权限。
同时要区分读写账号和只读账号。查询类接口使用只读账号,写入类接口使用读写账号。这样即使某个查询接口被注入,攻击者也无法修改数据。对于后台管理功能,可以使用独立的、权限更高的账号,但必须限制来源IP和增加二次验证。
上线前用真实攻击向量做验证
代码改造完成后,不能只靠人工审查或自动化扫描就宣布安全。需要构造针对性的注入测试用例,覆盖数字型、字符型、搜索型、排序型、分页型、登录型等常见注入点。测试用例应包括:单引号测试、双引号测试、反斜杠测试、宽字节测试、注释符测试、UNION SELECT测试、布尔盲注测试、时间盲注测试、堆叠查询测试等。
对于每个输入点,先发送正常请求确认功能不受影响,再发送包含特殊字符的请求观察响应。如果响应时间异常变长,可能存在时间盲注;如果返回了数据库错误信息,说明错误处理机制不完善;如果返回了非预期数据,说明过滤或参数化未生效。所有发现的问题应记录并回归修复,直到测试用例全部通过。
部署后持续监控SQL异常
安全升级不是一次性项目,上线后需要持续监控。数据库层面可以开启慢查询日志和错误日志,重点关注包含大量单引号、注释符、UNION关键字的SQL语句。应用层面可以在统一数据访问层增加日志,记录所有SQL执行语句和参数,但要注意日志中不能记录敏感信息,例如密码、令牌、身份证号等。对于异常频繁的同类请求,应触发告警。
同时可以部署Web应用防火墙作为补充防线,但WAF不能替代代码修复。老旧系统在完成代码级修复之前,WAF可以临时拦截明显的注入攻击,但攻击者仍然可能通过编码绕过、分块传输、参数污染等方式绕过。因此WAF只能作为过渡手段,最终必须依赖代码层的参数化和输入校验。
制定回归测试和灰度发布计划
老旧系统改造风险高,尤其是数据访问层的大规模修改可能影响正常业务。升级方案必须包含完整的回归测试计划。测试范围应覆盖所有涉及数据库读写的功能模块,包括登录、注册、搜索、列表、详情、导出、报表、后台管理等。测试数据要包含正常数据、边界数据、特殊字符数据、超长数据、空数据等。
灰度发布时先选择流量较小的节点或子模块上线,观察错误率、响应时间、数据库负载等指标。确认稳定后再逐步扩大范围。对于无法一次性改造完成的大型系统,可以按模块分批推进,但每一批都要完成从定位、改造、测试到上线的完整闭环,避免出现半改造状态导致防护不一致。
老旧代码系统的SQL注入安全升级,核心在于把“拼接SQL”这种历史惯性彻底打破,用参数化查询和统一数据访问层替代散落的字符串拼接。输入校验前置、数据库权限收敛、上线后持续监控是保障改造效果的三道附加防线。整个过程需要开发、测试、运维和数据库管理员协同配合,才能在不影响业务连续性的前提下,把SQL注入风险降到可接受范围。
