【Java代码审计】SQL注入篇

本文探讨了Java中的SQL注入问题,包括Statement执行SQL的漏洞、PreparedStatement的潜在风险、MyBatis中的安全问题如${}使用、order by注入、模糊查询注入和in子句注入。并给出了相应的防御策略,强调了正确使用参数化查询和安全编码的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.SQL注入漏洞概述

SQL 注入(SQL Injection)是因为程序未能正确对用户的输入进行检查,将用户的输入以拼接的方式带入 SQL 语句中,导致了 SQL 注入的产生。黑客通过 SQL 注入可直接窃取数据库信息,造成信息泄露

造成 SQL 注入一般需要满足以下两个条件:

  • 输入用户可控
  • 直接或间接拼入 SQL语句执行

因 SQL 注入漏洞特征性较强,在实际的审计过程中我们往往可以通过一些自动化审计工具快速地发现这些可能存在安全问题的代码片,如使用奇安信代码卫士、Fortify 等自动化工具

Java 语言本身是一种强类型语言,因此在寻找 SQL 注入漏洞的过程中,可以首先找到所有包含 SQL 语句的点,随后观察传参类型是否是 String 类型,只有当传参类型是 String 类型时我们才可能进行 SQL 注入


2.JDBC Statement执行SQL语句导致SQL注入

java.sql.Statement是Java JDBC下执行SQL语句的一种原生方式,执行语句时需要通过拼接来执行。若拼接的语句没有经过过滤,将出现SQL注入漏洞

下面的例子中,我们直接将用户输入的用户名和密码拼接到SQL查询中,而没有使用参数化查询或预编译语句。这样的代码容易受到SQL注入攻击,因为恶意用户可以在输入中插入恶意SQL代码,从而修改SQL查询的行为:

@RestController
public class InsecureController {

    @Autowired
    private DataSource dataSource;

    @GetMapping("/insecure-login")
    public String insecureLogin(@RequestParam("username") String username, @RequestParam("password") String password) {
        String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'";
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            if (rs.next()) {
                return "Login successful!";
            } else {
                return "Invalid username or password!";
            }
        } catch (SQLException e) {
            e.printStackTrace();
            return "An error occurred during login.";
        }
    }
}

假设用户输入了以下恶意输入:

用户名:admin' OR '1'='1
密码:任意密码

当这样的输入被拼接到SQL查询中时,查询语句会变成:

SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='任意密码'

这个查询会返回所有用户信息


3.PreparedStatement执行SQL语句但依然采用拼接

JDBC 有两种方法执行 SQL 语句,分别为 PrepareStatement 和 Statement。两个方法的区别在于 PrepareStatement 会对 SQL 语句进行预编译,而 Statement 方法在每次执行时都需要编译,会增大系统开销。理论上 PrepareStatement 的效率和安全性会比 Statement 要好,但并不意味着使用 PrepareStatement 就绝对安全,不会产生 SQL注入

例如下面的例子:

@RestController
public class InsecureController {

    @Autowired
    private DataSource dataSource;

    @GetMapping("/insecure-login")
    public String insecureLogin(@RequestParam("username") String username, @RequestParam("password") String password) {
        String sql = "SELECT * FROM users WHERE username='" + username + "' AND password=?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, password);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return "Login successful!";
                } else {
                    return "Invalid username or password!";
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
            return "An error occurred during login.";
        }
    }
}

在这个例子中,我们使用了PreparedStatement来执行查询,将密码作为参数传递给了PreparedStatement,但用户名仍然被拼接到SQL查询语句中,此时进行预编译则无法阻止 SQL 注入的产生


4.MyBatis使用了不安全的${}

MyBatis 中使用 parameterType 向 SQL 语句传参,在 SQL 引用传参可以使用#{Parameter}${Parameter}两种方式。#{Parameter}采用预编译的方式构造 SQL,避免了 SQL 注入的产生。而${Parameter}采用拼接的方式构造 SQL,在对用户输入过滤不严格的前提下,此处很可能存在 SQL 注入

例如:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
    <select id="getUserByUsernameAndPassword" resultType="com.example.User">
        SELECT * FROM users WHERE username = '${username}' AND password = '${password}'
    </select>
</mapper>

5.MyBatis order by注入

漏洞原因

order by 子句不能使用参数化查询的方式,只能使用字符拼接的方式,而在MyBatis中#{}是进行参数化查询的,如果在MyBatis的 order by 子句中使用#{},则 order by 子句会失效,所以要使用 order by 子句只能使用${}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
    <select id="getUsersOrderedBy" resultType="com.example.User">
        SELECT * FROM users
        <if test="orderBy != null">
            ORDER BY ${orderBy}
        </if>
    </select>
</mapper>

假设用户输入了以下恶意输入:

orderBy参数:id; DROP TABLE users;

如果将这个恶意输入作为orderBy参数传递给上述示例中的接口,那么生成的SQL查询语句将会是:

SELECT * FROM users ORDER BY id; DROP TABLE users;

这个查询会先按照id字段排序用户表,然后执行一个额外的恶意SQL语句,将users表删除

防御方法

解决order by注入的一个有效方法,就是将order by后面的内容写死,用户不可控,也就触发不了SQL注入了:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
    <select id="getUsersOrderedById" resultType="com.example.User">
        SELECT * FROM users
        ORDER BY id
    </select>
</mapper>

6.MyBatis 模糊查询注入

漏洞原因

%_模糊查询,MyBatis的like子句中使用#{}程序会报错,例如:“select * from users where name like '%#{user}%'”;为了避免报错只能使用${},例如:“select * from users where name like '%${user}%'”;但${}可能会存在SQL注入漏洞,要避免SQL注入漏洞就要进行过滤

例如,下面是一个有漏洞的代码:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
    <select id="getUserByName" resultType="com.example.User">
        SELECT * FROM users WHERE name LIKE '%${name}%'
    </select>
</mapper>

假设用户输入了以下恶意输入:

name参数:'; DROP TABLE users; --

如果将这个恶意输入作为name参数传递给上述示例中的接口,那么生成的SQL查询语句将会是:

SELECT * FROM users WHERE name LIKE '%'; DROP TABLE users; -- %'

这个查询会先匹配一个空字符串,然后执行一个额外的恶意SQL语句,将users表删除

防御方法

防御这种类型的攻击,一种有效的方式是强制数据类型,使用 ${} 本身是存在注入的,但由于强制使用Integer或long类型导致注入无效(无法注入字符串)

当然,最有效的方式,还是使用 #{} 安全编码,不过为了语法的正确性,要采用CONCAT函数进行拼接语句:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
    <select id="getUserByName" resultType="com.example.User">
        SELECT * FROM users WHERE name LIKE CONCAT('%', #{name}, '%')
    </select>
</mapper>

7.MyBatis in 子句注入

漏洞原因

在 MyBatis 的 in 子句中使用#{}会将多个参数当作一个整体,这偏离了原来的程序设计逻辑,无法查到数据,为了避免这个问题,只能使用${}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
    <select id="getUsersByIds" resultType="com.example.User">
        SELECT * FROM users WHERE id IN (${ids})
    </select>
</mapper>

假设用户输入了以下恶意输入:

ids参数:1, 2, 3); DROP TABLE users; --

如果将这个恶意输入作为ids参数传递给上述示例中的接口,那么生成的SQL查询语句将会是:

SELECT * FROM users WHERE id IN (1, 2, 3); DROP TABLE users; --

这个查询会先查询id为1、2、3的用户,然后执行一个额外的恶意SQL语句,将users表删除

防御方法

MyBatis提供了标签,用于安全地处理集合或数组类型的参数。通过标签,可以避免使用${},从而防止SQL注入,例如:

<select id="selectByIds" resultType="java.util.HashMap">
  SELECT * FROM my_table WHERE id IN
  <foreach item="id" collection="idList" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

如果必须通过动态SQL拼接来实现,应该结合MyBatis的动态SQL标签,如、等,尽量避免直接使用${}。如果实在无法避免,也应确保传入的数据已经经过彻底的输入验证和清理


8.JavaSQL注入审计关键词

statement
${
select
update
insert
delete
order
package com.tarena.dingdang.filter; 02 03 import java.io.IOException; 04 import java.util.Enumeration; 05 06 import javax.servlet.Filter; 07 import javax.servlet.FilterChain; 08 import javax.servlet.FilterConfig; 09 import javax.servlet.ServletException; 10 import javax.servlet.ServletRequest; 11 import javax.servlet.ServletResponse; 12 import javax.servlet.http.HttpServletRequest; 13 14 public class AntiSqlInjectionfilter implements Filter { 15 16 public void destroy() { 17 // TODO Auto-generated method stub 18 } 19 20 public void init(FilterConfig arg0) throws ServletException { 21 // TODO Auto-generated method stub 22 } 23 24 public void doFilter(ServletRequest args0, ServletResponse args1, 25 FilterChain chain) throws IOException, ServletException { 26 HttpServletRequest req=(HttpServletRequest)args0; 27 HttpServletRequest res=(HttpServletRequest)args1; 28 //获得所有请求参数名 29 Enumeration params = req.getParameterNames(); 30 String sql = ""; 31 while (params.hasMoreElements()) { 32 //得到参数名 33 String name = params.nextElement().toString(); 34 //System.out.println("name===========================" + name + "--"); 35 //得到参数对应值 36 String[] value = req.getParameterValues(name); 37 for (int i = 0; i < value.length; i++) { 38 sql = sql + value[i]; 39 } 40 } 41 //System.out.println("============================SQL"+sql); 42 //有sql关键字,跳转到error.html 43 if (sqlValidate(sql)) { 44 throw new IOException("您发送请求中的参数中含有非法字符"); 45 //String ip = req.getRemoteAddr(); 46 } else { 47 chain.doFilter(args0,args1); 48 } 49 } 50 51 //效验
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世界尽头与你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值