一、SQL 注入漏洞核心原理

后端程序将用户输入的内容,直接拼接到 SQL 语句中执行,导致攻击者可以通过构造恶意输入,篡改 SQL 语句的执行逻辑,达到窃取数据、控制数据库的目的

二、SQL 注入漏洞的产生原因

1. 最常见的核心原因:直接拼接 SQL 语句

// 高危错误写法(直接拼接用户输入,必出注入)
String username = request.getParameter("username");
String sql = "select * from user where username = '" + username + "'"; // 拼接输入
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql); // 直接执行拼接后的SQL

这种写法没有对用户输入做任何处理,攻击者只要输入恶意字符串,就能篡改 SQL 逻辑。

所以在url中如果有数据库查询参数,直接就gg。

2. 未对用户输入做过滤、校验、转义

比如:

  • 不校验输入格式(比如 ID 应该是数字,却允许输入字母、符号);
  • 不转义特殊字符(比如单引号、分号、逗号等 SQL 关键符号);
  • 过滤不彻底(只过滤部分字符,留下绕过空间);

3. 使用不安全的数据库执行方式

Java 开发中,Statement 类是不安全的,它会直接执行拼接后的 SQL 语句,不做任何防注入处理;而很多开发习惯使用 Statement,而非安全的 PreparedStatement(预编译语句)。

同理,使用MyBatis框架也是一样的需要预编译的哦。

4. 数据库权限过大

这个其实很容易被忽略,但是危害很大

很多项目为了开发方便,直接使用数据库的 “超级管理员账号”(比如 MySQL 的 root 账号)运行,一旦出现 SQL 注入,攻击者可以利用超级权限执行删库(drop database)、写文件(into outfile)等高危操作,造成不可挽回的损失。

5. 开发规范缺失

这个就是需要开发、测试的人员好好使用好的规范的,比如阿里的规范什么的,具体的比如:

  • 随意使用动态 SQL 拼接、动态表名、动态排序字段;
  • 对敏感操作(比如删数据、改密码)没有额外校验;
  • 测试不全面,只测试正常输入,不测试恶意输入。

三、SQL 注入漏洞的常见类型

面试经常会问到,要好好记忆哦。

1. 联合查询注入(最常用,有回显)

攻击者通过 union select 拼接查询语句,获取数据库中的其他数据(库名、表名、字段名、敏感数据)。

正常请求:http://xxx.com/user?id=1 这些都是在get的url里面或者post里面或者用burb来测

对应的 SQL:select * from user where id=1

恶意请求:http://xxx.com/user?id=1 union select 1,2,database(),user()

拼接后的 SQL:select * from user where id=1 union select 1,2,database(),user()

就可以直接查到数据库的敏感信息

2. 报错注入(无需复杂操作,有报错)

适用场景:页面会显示 SQL 执行错误信息(比如数据库报错、代码异常),攻击者通过构造 “会报错的 SQL 语句”,让报错信息中包含敏感数据(比如库名、表名)。

恶意请求:http://xxx.com/user?id=1 and updatexml(1,concat(0x7e,user(),0x7e),1)报错信息会显示:XPATH syntax error: '~root@localhost~',直接泄露数据库用户名。

核心逻辑:利用数据库的报错函数(updatexml、extractvalue 等),将敏感数据拼接在报错信息中,实现 “无回显也能拿数据”。

3. 盲注 (这个考得也很多,分两种)

布尔盲注:通过 and 1=1(页面正常)、and 1=2(页面异常),判断注入点是否存在,再逐步推测数据;

时间盲注:通过 and sleep(5)(页面延迟 5 秒加载),判断条件是否成立,适合完全无回显的场景。

4. 堆叠注入(执行多条 SQL)

适用场景:后端程序允许执行多条 SQL 语句(用分号分隔),攻击者可以拼接多条 SQL,实现删库、改数据等高危操作。

select * from user where id=1; drop table user; -- (太危险了,不要在公网尝试)

5. 读写文件注入(提权用,渗透用)

适用场景:数据库账号拥有 “文件读写权限”(比如 MySQL 的 FILE 权限),攻击者可以通过 SQL 语句读取服务器文件(比如配置文件、密码文件),或写入恶意文件(比如木马)。

  • 读文件:select load_file('/etc/passwd')(读取 Linux 系统的用户密码文件);
  • 写文件:select '<?php eval($_POST[cmd]);?>' into outfile '/var/www/html/webshell.php'(写入 PHP 木马)。

写完就可以蚁剑了

6. 越权注入(业务场景高频,算是逻辑漏洞)

适用场景:通过注入绕过登录校验、权限控制,比如:

  • 登录页面,输入用户名 admin' or 1=1 --,密码任意,即可登录管理员账号;
  • 查看他人数据,输入 id=100 or 1=1,即可查询所有用户的信息。有横向、纵向。

四、SQL 注入的过滤绕过思路

  • 大小写绕过:比如 union select 改成 UnIoN SeLeCt,绕过大小写敏感的过滤;
  • 编码绕过:将恶意字符进行 URL 编码、十六进制编码,比如 ' 编码成 %27,绕过直接过滤;
  • 注释绕过:用 /**/ 代替空格,比如 1/**/and/**/1=1,绕过 “过滤空格” 的限制;
  • 双写绕过:如果过滤了 union,就写成 UNIunionON,过滤后会剩下 union
  • 特殊字符替换:用 || 代替 or,用 && 代替 and,绕过关键词过滤;
  • 括号 / 换行混淆:比如 select(1)from(user)where(id=1),用括号打乱语句结构,绕过过滤。

五、Java 项目代码层防御

作为 Java 安全开发,SQL 注入的防御是核心能力,也是面试、代码审计的必考点,以下 5 种方法,覆盖所有 Java 开发场景(JDBC、MyBatis、MyBatis-Plus),代码可直接复制使用。

1. 核心防御:使用预编译语句

(1)JDBC 场景(虽然感觉没什么人使用了...泪目)

String username = request.getParameter("username");
String sql = "select * from user where username = '" + username + "'"; // 拼接输入
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql); // 直接执行,有注入风险

String username = request.getParameter("username");
// 1. 预编译SQL,用?占位符代替参数
String sql = "select * from user where username = ?";
// 2. 使用PreparedStatement(预编译语句)
PreparedStatement pst = conn.prepareStatement(sql);
// 3. 传入参数(自动转义特殊字符)
pst.setString(1, username); // 第一个?对应username参数
// 4. 执行查询
ResultSet rs = pst.executeQuery();

(2)MyBatis 场景(最常用)



MyBatis 中,#{} 和 ${} 的区别,直接决定了是否存在注入,这是 Java 开发最容易踩的坑:

错误写法(高危,${} 直接拼接):
<!-- 高危!${} 会直接拼接用户输入,存在注入 -->
<select id="getUserByUsername" parameterType="String" resultType="User">
    select * from user where username = '${username}'
</select>

正确写法(安全,#{} 预编译):
<!-- 安全!#{}` 会自动预编译,将输入作为参数传入,防注入 -->
<select id="getUserByUsername" parameterType="String" resultType="User">
    select * from user where username = #{username}
</select>

核心提醒:MyBatis 中,** 永远使用 #{ },禁止使用 ${ }**;如果必须使用 ${ }(比如动态表名、动态排序字段),一定要做白名单校验。

2. 辅助防御:严格做参数校验

预编译是核心,但参数校验能进一步降低风险,Java 中可使用 Validation 框架,统一校验参数:

3. 兜底防御:禁止动态拼接、动态表名 / 排序

如果业务必须使用动态表名、动态排序字段(比如根据用户选择排序),禁止直接拼接用户输入,必须使用 “白名单校验”,比如使用枚举什么的

4. 权限防御:数据库最小权限原则

即使出现 SQL 注入,也要通过 “权限控制”,降低攻击造成的损失,可以使用如下方法

  • 项目使用的数据库账号,禁止使用 root、admin 等超级管理员账号
  • 给数据库账号分配 “最小权限”:只给 “查询、插入、更新” 权限,禁止 “删除、修改表结构、文件读写” 权限;
  • 生产环境的数据库,禁止远程连接,只允许项目服务器本地连接;
  • 敏感表(比如用户表、密码表),单独设置权限,禁止普通业务账号访问。

5. 工具防御:使用代码扫描工具

Java 项目可集成 SonarQube、CheckMarx 等代码扫描工具,在开发、测试阶段,自动检测 “SQL 注入漏洞”(比如拼接 SQL、使用 ${}),提前整改,避免漏洞上线。

六、企业级整体防护体系

结合企业实际的生产,我们可以从五个方面进行防护

  • 开发层:制定 Java 开发规范,强制使用预编译语句,禁止拼接 SQL,定期开展安全培训;
  • 测试层:专门开展 “SQL 注入专项测试”,不仅测试正常输入,还要测试恶意输入,覆盖所有接口;
  • 生产层:部署 WAF(Web 应用防火墙),拦截典型的 SQL 注入攻击(比如包含 union、drop、sleep 等关键词的请求);
  • 审计层:部署 SQL 审计插件,实时监控数据库执行的 SQL 语句,发现异常注入行为,及时告警;
  • 运维层:定期更新数据库补丁,清理无用的数据库账号,回收过量权限,定期备份数据(防止删库后无法恢复)。

七、总结

对于 Java 安全开发而言,记住 3 个核心原则,就能规避所有 SQL 注入漏洞:

  • 永远不要拼接 SQL 语句,永远使用 PreparedStatement 或 MyBatis 的 #{}
  • 永远对用户输入做校验,过滤特殊字符、限制输入格式;
  • 永远给数据库账号分配最小权限,做好权限兜底。

阳君后续将持续解析 XSS、CSRF 等常见网安漏洞,结合 Java 开发场景,分享实战防御方案,助力大家既吃透漏洞原理,又能落地安全实践。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐