4. 注入类漏洞防御编码
注入类漏洞的本质,是程序把“数据”错误地当成了“代码”或“查询结构”来执行。SQL注入、命令注入、LDAP注入虽然发生在不同层面,但根因高度一致:用户可控输入未经结构化处理,直接拼接进解释器可识别的语句中。防御的核心思路不是“过滤坏字符”,而是让数据永远停留在数据的位置,无法改变语句结构。本章围绕参数化查询、安全API封装、最小权限账户和危险函数替代四条主线展开。
一、SQL注入:参数化查询是首选方案
SQL注入的典型场景是字符串拼接。例如下面这段伪代码:
String sql = "SELECT id, name FROM users WHERE name = '" + userName + "'";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
当 userName 传入 ' OR '1'='1 时,查询条件被改写,返回全部用户。参数化查询(Prepared Statement)通过预编译语句模板,把 SQL 结构与参数值分离,数据库先解析语句结构,再绑定参数,参数内容不会参与语法解析。改写如下:
String sql = "SELECT id, name FROM users WHERE name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, userName);
ResultSet rs = ps.executeQuery();
需要特别注意的是,参数化只能用于值的位置,不能用于表名、列名、ORDER BY 字段等结构位置。若业务确实需要动态排序,应使用白名单映射,例如只允许 name、created_at 两个字段,其余一律拒绝,而不是把用户输入直接拼进 ORDER BY。
对于 LIKE 查询,参数化同样适用,但通配符需要由程序控制:WHERE name LIKE ?,绑定值为 "%" + escapeLike(userInput) + "%",其中 escapeLike 负责转义 %、_ 和转义符本身。这样既保留模糊匹配能力,又不破坏参数化边界。
二、命令注入:避免拼接,优先使用参数数组
命令注入常出现在调用系统命令的场景。危险写法如 Runtime.getRuntime().exec("ping -c 1 " + ip),当 ip 为 127.0.0.1; rm -rf / 时,第二条命令会被执行。防御方式是使用支持参数数组的 API,让每个参数独立传递,不经过 shell 解析:
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", ip);
pb.redirectErrorStream(true);
Process p = pb.start();
如果必须使用 shell,则应对输入做严格白名单校验,例如 IP 地址只允许数字和点,长度不超过 15,且用正则 ^(\d{1,3}\.){3}\d{1,3}$ 校验每一段范围。更稳妥的做法是彻底避免调用外部命令,改用语言内置库或受控的服务接口。
三、LDAP注入:转义与过滤器构造
LDAP 查询过滤器同样存在注入风险。例如 (uid=" + user + "),当 user 为 *)(uid=*))(|(uid=* 时,过滤器逻辑被篡改。防御方式是对特殊字符进行转义,包括 * ( ) \ NUL 等,并使用框架提供的过滤器构造方法,而不是手工拼接。例如 Java 中可使用 Filter.encodeValue(),或采用白名单限制 uid 只允许字母数字。
四、最小权限数据库账户
即使代码存在疏漏,最小权限账户也能限制攻击后果。应用连接数据库的账户不应使用 root 或 sa,而应只授予业务必需权限。例如一个只读查询服务,账户权限可配置为:
GRANT SELECT ON app_db.users TO 'app_read'@'10.0.0.%';
GRANT SELECT ON app_db.orders TO 'app_read'@'10.0.0.%';
-- 不授予 INSERT、UPDATE、DELETE、DROP、FILE 等权限
对于需要写入的模块,单独使用另一个账户,并限制可操作的表和列。这样即使发生注入,攻击者也无法通过 LOAD_FILE、INTO OUTFILE 或跨库查询扩大战果。同时应关闭数据库的 multiStatements 选项,避免一次注入执行多条语句。
五、安全API封装与危险函数替代
把安全操作封装成统一 API,可以降低团队犯错概率。例如封装一个 SafeQuery 类,内部强制使用 PreparedStatement,对外只暴露“SQL 模板 + 参数列表”的接口,禁止传入完整 SQL 字符串。对于命令执行,封装 SafeCommand,只接受命令名和参数数组,并在内部做白名单校验。
危险函数替代清单可参考:
Statement.execute→PreparedStatement.executeRuntime.exec(String)→ProcessBuilder(List<String>)InitialDirContext.search手工拼接 → 使用过滤器编码方法String.format拼 SQL → 参数占位符
contains(";") 判断命令注入,攻击者可用换行、管道、反引号等多种方式绕过。正确做法是“只允许已知安全字符”,而不是“拒绝已知危险字符”。
综合来看,注入防御的落地顺序建议是:先全面替换为参数化查询和安全 API,再收紧数据库账户权限,最后补充输入白名单和运行时监控。三层叠加后,即使某一层被绕过,其余层仍能限制影响范围。编码阶段就应把“数据与结构分离”作为默认习惯,而不是等到渗透测试报告出来再补救。