SQL注入修复后被绕过的原因有很多,其中多重编码解码问题是开发者容易忽略的一类场景。很多防护逻辑仅对用户输入做一次解码和过滤,攻击者可以通过多次编码输入内容,让第一次解码后的内容仍然处于编码状态,从而绕过预设的过滤规则,最终执行恶意SQL语句。
多重编码解码绕过原理
常见的编码方式包括URL编码、HTML实体编码、十六进制编码等,很多Web应用会在接收请求时自动做一次解码,比如Servlet容器会自动解码URL编码的参数。如果开发者在防护时仅对解码后的内容做一次过滤,攻击者可以将攻击 payload 做两次URL编码,第一次自动解码后 payload 仍处于编码状态,不会被过滤规则匹配,后续如果再有一次解码操作,恶意内容就会被还原,从而触发SQL注入。
常见编码场景示例
以URL编码为例,单引号'的一次编码结果是%27,两次编码结果是%2527。如果应用仅解码一次,%2527会被解码为%27,此时过滤规则如果仅检测'和%27,就会遗漏这个编码后的内容,后续如果应用再对参数做一次URL解码,就会还原出单引号,拼接SQL时就会引发注入。
如何检测多重编码问题
检测可以从以下几个维度入手:
- 梳理应用所有涉及输入解码的环节,统计一共有几次解码操作,确认防护逻辑的解码次数是否匹配
- 构造多次编码的测试 payload,比如两次URL编码的SQL注入 payload,提交后观察是否能被过滤规则拦截
- 检查输入过滤逻辑是否仅针对特定编码格式,是否忽略了其他编码方式或者多次编码的场景
修复方案与代码示例
核心思路是统一解码入口,仅做一次完全解码后再做过滤,避免多次解码导致编码内容残留。以下是Java语言的修复示例:
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public class SqlInjectionFix {
// 统一解码方法,仅做一次完整解码
public static String decodeInput(String input) {
if (input == null) {
return null;
}
// 仅做一次URL解码,避免多次解码
return URLDecoder.decode(input, StandardCharsets.UTF_8);
}
// 过滤SQL特殊字符的方法
public static String filterSqlInput(String input) {
if (input == null) {
return null;
}
// 先统一解码
String decodedInput = decodeInput(input);
// 替换常见的SQL注入特殊字符
return decodedInput.replace("'", "")
.replace(""", "")
.replace("\", "")
.replace(";", "")
.replace("(", "")
.replace(")", "");
}
public static void main(String[] args) {
// 模拟两次编码的单引号输入 %2527
String maliciousInput = "%2527";
String filtered = filterSqlInput(maliciousInput);
System.out.println("过滤后结果:" + filtered);
// 输出为空,说明单引号被成功过滤
}
}
其他注意事项
除了统一解码逻辑,还可以结合预编译语句来从根源上避免SQL注入,预编译语句会把用户输入作为参数处理,不会拼接为SQL语法的一部分,即使有编码绕过也无法改变SQL的结构。以下是使用预编译的示例:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class SafeSqlQuery {
public void queryUser(String username) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = getConnection();
// 使用预编译语句,?是参数占位符
String sql = "SELECT * FROM user WHERE username = ?";
ps = conn.prepareStatement(sql);
// 设置参数,用户输入不会被解析为SQL语法
ps.setString(1, username);
rs = ps.executeQuery();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源逻辑省略
}
}
private Connection getConnection() {
// 获取数据库连接的逻辑省略
return null;
}
}
另外需要定期做渗透测试,覆盖多重编码的攻击场景,及时更新过滤规则,确保防护逻辑能够覆盖新的绕过方式。同时尽量避免手动拼接SQL语句,优先使用ORM框架或者预编译语句,从根源上降低SQL注入风险。