对JAVA的SQL注入的思考

xiao1star2026-01-29文章来源:SecHub网络安全社区


前言

之前大部分对sql注入的研究都是在PHP代码中,最近想要了解java代码中的sql注入又是怎么形成呢,因此就开启来java的sql注入的探索之旅

mybatis

mybatis标志

在其xml文件中出现mybatis的字样

202407141518455.png

${}#{}的性质

#{}

  • #{} 底层通过prepareStatement对当前传入的sql进行了预编译,一个 #{ } 被解析为一个参数占位符 ?;
  • #{} 解析之后会将String类型的数据自动加上引号,其他数据类型不会
  • #{} 很大程度上可以防止sql注入(sql注入是发生在编译的过程中,因为恶意注入了某些特殊字符,最后被编译成了恶意的执行操作)
  • #{} 一般用在insert的字段和where条件中,用来防止sql注入

${}

  • ${}仅仅为一个纯粹的 string 替换,在动态sql解析阶段将会进行变量替换
  • ${} 解析之后是什么就是什么
  • ${} 用在sql字符串拼接中,使用时需要非常谨慎。但是像一些没有直接和系统用户接触的功能如动态切换表名,库名呀就不存在注入问题了。一旦要使用在要被用户直接接触的sql中,一定要注意!

代码调用过程

二者对比

${}的使用

在登录框中输入正确的账号密码

admin
1

输入之后控制台输出的数据

202407132242525.png

当输入危险字符串

admin' or '1'='1
密码随便

202407132246886.png

202407132245403.png

202407132203003.png

#{}的使用

当登录框中输入如下

admin 
1

202407132255899.png

当登录框中输入

admin' or '1'='1
密码随便

202407132255150.png

发现并没有造成闭合形成攻击

为什么不弃用${}

1.在order by语句中如果使用#{},例如输入

<select id="queryByParams" parameterType="MyTestDO" resultMap="MyTestMap"> SELECT * FROM users order by #{ID} </select>

那么最后的sql语句会变为SELECT * FROM users order by 'ID'

而在sql语句中虽然select * from users order by 'ID'可以执行但是执行的结果并不是我们想要的

如下图所示,发现select * from users order by 'id'并没有按顺序排列

202407141008162.png

2.还需要注意的是,其实like语句是可以使用#{}执行的

<select id="queryUserByName" parameterType="String" resultType="com.entity.User"> SELECT * FROM users WHERE username like concat('%',#{value},'%') </select>

202407141014918.png

JDBC

JDBC使用标志

JDBC编程步骤:

  • 1、加载驱动(Driver)
  • 2、获取连接(Connection)
  • 3、创建Statement对象(Statement、PreparedStatement)
  • 4、执行SQL(execute、executeUpdate、executeQuery)
  • 5、释放资源

可以去工具层(until)或者mapper层中查看是否出现com.mysql.jdbc.Driver的驱动名字,或者使用DriverManager.getConnection()去连接数据库

不安全代码及分析

@RequestMapping("/dynamic") public String jdbcdynamic(@RequestParam("id") String id) throws ClassNotFoundException, SQLException { StringBuilder result = new StringBuilder(); Class.forName(driver); Connection conn = DriverManager.getConnection(url, user, password);//连接数据库 Statement statement = conn.createStatement();//创建一个sql对象 String sql = "select * from user where id = '" + id + "'"; ResultSet rs = statement.executeQuery(sql);//执行sql语句 while (rs.next()) { String rsUsername = rs.getString("username"); String rsPassword = rs.getString("password"); String info = String.format("%s: %s\n", rsUsername, rsPassword); result.append(info); } rs.close(); conn.close(); return result.toString(); }

分析

上述代码中使用了jdbc来进行数据库的链接,在执行的sql语句为select * from user where id = '" + id + "',通过由前端获得的id值直接拼接的形式完成了sql语句的执行,当我们在输入' or '1'='1 时就会形成select * from user where id = '' or '1'='1'从而形成闭合完成注入

安全的代码及其分析

@RequestMapping("/dynamic") public String jdbcDynamic(@RequestParam("id") String id) throws ClassNotFoundException, SQLException { StringBuilder result = new StringBuilder(); Class.forName(driver); // 加载数据库驱动 Connection conn = DriverManager.getConnection(url, user, password); // 连接数据库 // 使用预处理语句 String sql = "SELECT * FROM user WHERE id = ?"; //?为占位符 PreparedStatement preparedStatement = conn.prepareStatement(sql); preparedStatement.setString(1, id); // 设置SQL语句中的参数 ResultSet rs = preparedStatement.executeQuery(); // 执行查询 while (rs.next()) { String rsUsername = rs.getString("username"); String rsPassword = rs.getString("password"); String info = String.format("%s: %s\n", rsUsername, rsPassword); result.append(info); } // 关闭资源 rs.close(); preparedStatement.close(); conn.close(); return result.toString(); }

分析

在上述代码中使用了预处理的机制,使用了prepareStatement函数对sql语句进行了处理

  • conn.createStatement() 方法用于创建一个 Statement 对象,该对象用于执行静态的 SQL 语句。由于它直接接受完整的 SQL 语句作为字符串,因此如果 SQL 语句中包含来自用户输入的数据,那么这些数据可能会被恶意地修改以执行未授权的 SQL 命令,这就是所谓的 SQL 注入攻击。
  • conn.prepareStatement() 方法用于创建一个 PreparedStatement 对象,该对象也用于执行 SQL 语句,但它允许你将 SQL 语句中的某些部分(如条件值)作为参数来设置,而不是直接嵌入到 SQL 语句字符串中。这样做可以防止 SQL 注入,因为即使参数值包含恶意的 SQL 片段,数据库也会将它们视为普通的数据值而不是 SQL 代码来执行。

当我们输入的id值为' or '1'='1时,会经过预处理之后,直接会整个' or '1'='1当作一个字符串去进行查询操作,从而有效的避免sql注入

Hibernate

Hibernate是一个基于jdbc的开源的持久化框架,是一个优秀的ORM实现,它很大程度的简化了dao层编码工作。Hibernate对JDBC访问数据库的代码做了封装,大大简化了数据访问层繁琐的重复性代码。
在分层结构中处于持久化层,封装对数据库的访问细节,使业务逻辑层更专注于实现业务逻辑。


使用Hibernate标志

查看其xml中的文件是否出现后Hibernate的字样

202407141526023.png

不安全代码及分析

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); Configuration configuration = new Configuration(); configuration.configure(); // 确保你有正确的配置文件路径 SessionFactory sessionFactory = configuration.buildSessionFactory(); String input = request.getParameter("sqlin");//从前端页面获取参数值 String hqlString = "select * from Classes where name='" + input + "'"; // sql语句 Session session = sessionFactory.openSession(); List<Classes> classesList = session.createQuery(hqlString).list(); //执行sql语句 for (Classes clazz : classesList) { response.getWriter().write(clazz.getName()); response.getWriter().print("<br/>"); } session.close(); // 在循环外关闭 Session }

分析

  • hqlString变量中,sql语句是直接拼接而成的,没有任何限制,完全可以通过闭合单引号来形成闭合从而造成注入

安全代码

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); // 配置Hibernate Configuration configuration = new Configuration(); configuration.configure(); // 确保你有正确的配置文件路径 SessionFactory sessionFactory = configuration.buildSessionFactory(); // 获取用户输入 String input = request.getParameter("sqlin"); // 使用参数化查询来防止HQL注入 String hqlString = "select * from Classes where name = :name"; Session session = sessionFactory.openSession(); Query query = session.createQuery(hqlString); query.setParameter("name", input); // 使用setParameter来设置参数值 // 执行查询并获取结果 List<Classes> classesList = query.list(); // 处理查询结果 for (Classes clazz : classesList) { response.getWriter().write(clazz.getName()); response.getWriter().print("<br/>"); } // 关闭Session session.close(); }

使用了命名参数:name来代替直接将input变量拼接到HQL字符串中。之后使用Query对象的setParameter方法来设置参数的实际值。这样,Hibernate就可以安全地处理这个值,而不必担心它包含恶意代码或SQL/HQL注入攻击。