九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

admin 2024年2月16日23:55:30评论9 views字数 12218阅读40分43秒阅读模式

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

·前言·

Java Spring是目前企业开发中使用较多的一种java开发框架,因此基于该框架的安全内容尤为重要。Secure Code Warrior编码实验室的Java Spring涉及到的安全问题有9个,分别为缺少功能级别的访问控制、不恰当的身份验证、日志记录和监控不足、SQL注入、明文存储密码、路径遍历、服务器请求伪造、XML外部实体(XXE)、任意文件上传。

因涉及内容较多,完整内容将会在本公众号拆分为多篇内容分别发出。本文为该系列的第四篇——安全问题四:SQL注入。

往期内容请查看Java Spring编码安全系列

Parameterize queries using Java Persistence API (JPA)

使用 Java Persistence API (JPA) 参数化查询

Learn different ways to parameterize queries with native SQL queries and Spring Data query methods, in order to protect against SQL injection.

了解使用原生SQL 查询和 Spring Data 查询方法参数化查询的不同方法,以防止 SQL 注入。

安全问题四:SQL注入

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

题目

1、介绍

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

The CustomInvoiceRepositoryImpl contains a query that is vulnerable to SQL injection (SQLi), as it concatenates unvalidated user input in the query string. Dynamically building SQL queries with crafted input could return restricted information to the user or compromise the database's integrity.One of the most effective ways to combat this vulnerability is to use parameterized queries/prepared statements. These types of statements pre-compile SQL code with placeholders instead of the parameters. When the query is executed, the parameters are added as a separate statement.So, DROP what you're doing, and let's JOIN forces in beating SQLi! This lab will explore two different ways of writing secure queries.

CustomInvoiceRepositoryImpl 包含一个容易受到 SQL 注入 (SQLi) 攻击的查询,因为它在查询字符串中连接了未经验证的用户输入。使用精心设计的输入动态构建 SQL 查询可能会向用户返回受限信息或损害数据库的完整性。对抗此漏洞的最有效方法之一是使用参数化查询/准备语句。这些类型的语句使用占位符而不是参数来预编译 SQL 代码。执行查询时,参数将作为单独的语句添加。所以,放弃你正在做的事情,让我们联手打败 SQLi!本实验将探索编写安全查询的两种不同方法。

2、源码

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

package vikingbank.web;import jakarta.persistence.EntityManager;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Repository;import vikingbank.web.entities.Invoice;import vikingbank.web.repositories.CustomInvoiceRepository;import java.util.List;@Repositorypublic class CustomInvoiceRepositoryImpl implements CustomInvoiceRepository {    private final EntityManager entityManager;    private final JdbcTemplate jdbcTemplate;    public CustomInvoiceRepositoryImpl(EntityManager entityManager, JdbcTemplate jdbcTemplate) {        this.entityManager = entityManager;        this.jdbcTemplate = jdbcTemplate;    }    @Override    @SuppressWarnings("unchecked")    public List<Invoice> filterSentInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",                "SELECT i.* FROM invoice AS i",                "JOIN bank_account AS ba ON i.seller_id = " + bankAccountId + " AND i.buyer_id = ba.Id",                "WHERE ba.account_number LIKE '%" + bankAccountFilter + "%'");        return entityManager.createNativeQuery(query, Invoice.class)                            .getResultList();    }    @Override    public List<Invoice> filterReceivedInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",                "SELECT i.* FROM invoice AS i",                "JOIN bank_account AS ba ON i.seller_id = ba.id AND i.buyer_id = " + bankAccountId + "",                "WHERE ba.account_number LIKE '%" + bankAccountFilter + "%'");        return jdbcTemplate.query(query, Invoice::fromRow);    }}

*左右滑动查看更多

文件结构如下:

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

3、步骤一 使用JPA实体管理器

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

打开分配文件夹中的CustomInvoiceRepositoryImpl.java。VikingBank 的发票有买方和卖方字段。两者都与银行帐号相关联。filterSentInvoicesByAccountNumber 创建一个查询,该查询将获取买家银行号码与(部分)银行帐号匹配的所有发票作为过滤器。目前,正在连接用户输入。为了确保其安全,需要使用命名参数对其进行参数化。

3.1 task1
  • 将bankAccountId 连接替换为字符串:bankAccountId。在 Java Persistence API (JPA) 中,:name 表示命名参数。执行查询时,JPA 将在运行时插入用户输入。

  • BankAccountFilter 变量的参数化有点棘手,因为它是部分匹配。使用 SQL concat 函数对bankAccountFilter 进行参数化。

Hint"SELECT * FROM table as t WHERE t.column1 == :param1 AND t.column2 LIKE CONCAT('%',:param2,'%')"

*左右滑动查看更多

根据要求修改代码。

CustomInvoiceRepositoryImpl.javapackage vikingbank.web;import jakarta.persistence.EntityManager;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Repository;import vikingbank.web.entities.Invoice;import vikingbank.web.repositories.CustomInvoiceRepository;import java.util.List;@Repositorypublic class CustomInvoiceRepositoryImpl implements CustomInvoiceRepository {    private final EntityManager entityManager;    private final JdbcTemplate jdbcTemplate;    public CustomInvoiceRepositoryImpl(EntityManager entityManager, JdbcTemplate jdbcTemplate) {        this.entityManager = entityManager;        this.jdbcTemplate = jdbcTemplate;    }    /**     * 根据银行账户过滤已发送的发票     * @param bankAccountFilter 银行账户过滤条件     * @param bankAccountId 银行账户ID     * @return 过滤后的发票列表     */    @Override    @SuppressWarnings("unchecked")    public List<Invoice> filterSentInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",                "SELECT i.* FROM invoice AS i",                "JOIN bank_account AS ba ON i.seller_id = :bankAccountId AND i.buyer_id = ba.Id",                "WHERE ba.account_number LIKE CONCAT('%', :bankAccountFilter, '%')");        return entityManager.createNativeQuery(query, Invoice.class)                .setParameter("bankAccountId", bankAccountId)                .setParameter("bankAccountFilter", bankAccountFilter)                .getResultList();    }    /**     * 根据银行账户过滤已接收的发票     * @param bankAccountFilter 银行账户过滤条件     * @param bankAccountId 银行账户ID     * @return 过滤后的发票列表     */    @Override    public List<Invoice> filterReceivedInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",                "SELECT i.* FROM invoice AS i",                "JOIN bank_account AS ba ON i.seller_id = ba.id AND i.buyer_id = :bankAccountId",                "WHERE ba.account_number LIKE CONCAT('%', :bankAccountFilter, '%')");        return jdbcTemplate.query(query, Invoice::fromRow,                bankAccountId, bankAccountFilter);    }}

*左右滑动查看更多

3.2 task2
  • 命名参数仍然需要值,以便entityManager 可以准备和执行查询。将 setParameter 调用链接到每个参数的 createNativeQuery 调用上。

  • setParameter 调用采用两个参数,第一个是不带: 的命名参数,第二个是值。

.setParameter("name", value)
Step Solutionvar query = String.join(" ",    "SELECT i.* FROM invoice AS i",    "JOIN bank_account AS ba ON i.seller_id = :accountId AND i.buyer_id = ba.Id",    "WHERE ba.account_number LIKE CONCAT('%',:filter,'%')");return  entityManager.createNativeQuery(query, Invoice.class)        .setParameter("accountId", bankAccountId)        .setParameter("filter", bankAccountFilter)        .getResultList();

*左右滑动查看更多

按照要求修改:

CustomInvoiceRepositoryImpl.javapackage vikingbank.web;import jakarta.persistence.EntityManager;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Repository;import vikingbank.web.entities.Invoice;import vikingbank.web.repositories.CustomInvoiceRepository;import java.util.List;@Repositorypublic class CustomInvoiceRepositoryImpl implements CustomInvoiceRepository {    private final EntityManager entityManager;    private final JdbcTemplate jdbcTemplate;    public CustomInvoiceRepositoryImpl(EntityManager entityManager, JdbcTemplate jdbcTemplate) {        this.entityManager = entityManager;        this.jdbcTemplate = jdbcTemplate;    }    /**     * 根据银行账户过滤已发送的发票     *     * @param bankAccountFilter 银行账户过滤条件     * @param bankAccountId     银行账户ID     * @return 过滤后的发票列表     */    @Override    @SuppressWarnings("unchecked")    public List<Invoice> filterSentInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",                "SELECT i.* FROM invoice AS i",                "JOIN bank_account AS ba ON i.seller_id = :bankAccountId AND i.buyer_id = ba.Id",                "WHERE ba.account_number LIKE CONCAT('%', :bankAccountFilter, '%')");        return entityManager.createNativeQuery(query, Invoice.class)                .setParameter("bankAccountId", bankAccountId)                .setParameter("bankAccountFilter", bankAccountFilter)                .getResultList();    }    /**     * 根据银行账户过滤已接收的发票     *     * @param bankAccountFilter 银行账户过滤条件     * @param bankAccountId     银行账户ID     * @return 过滤后的发票列表     */    @Override    public List<Invoice> filterReceivedInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",                "SELECT i.* FROM invoice AS i",                "JOIN bank_account AS ba ON i.seller_id = ba.id AND i.buyer_id = :bankAccountId",                "WHERE ba.account_number LIKE CONCAT('%', :bankAccountFilter, '%')");        var jpaQuery = entityManager.createNativeQuery(query, Invoice.class);        jpaQuery.setParameter("bankAccountId", bankAccountId);        jpaQuery.setParameter("bankAccountFilter", bankAccountFilter);        return jpaQuery.getResultList();    }}

*左右滑动查看更多

分析一下代码内容。

该类是自定义发票仓库的实现类,实现了 CustomInvoiceRepository 接口。以下是对代码的详细注释:

  • @Repository 注解标记该类为仓库组件。

  • 定义了 CustomInvoiceRepositoryImpl 类,并声明了 EntityManager 和 JdbcTemplate 成员变量,并在构造函数中进行依赖注入。

  • @Override 注解表示该方法重写了接口中的方法。

  • filterSentInvoicesByAccountNumber 方法用于根据银行账户过滤已发送的发票。

  • filterReceivedInvoicesByAccountNumber 方法用于根据银行账户过滤已接收的发票。

  • @SuppressWarnings("unchecked") 注解用于抑制类型转换警告。

  • 在 filterSentInvoicesByAccountNumber 方法中,使用原生 SQL 查询语句拼接查询,并通过 EntityManager 执行查询,并将结果转换为 Invoice 实体对象。

  • 在 filterReceivedInvoicesByAccountNumber 方法中,使用原生 SQL 查询语句拼接查询,并通过 EntityManager 执行查询,并将结果转换为 Invoice 实体对象。

以上就是对上面代码的详细注释,该类实现了自定义的发票仓库接口,并提供了根据银行账户进行发票过滤的功能。

4、步骤二 使用Jdbc模板

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

接下来转到

filterReceivedInvoicesByAccountNumber 方法。此方法将查询卖方银行号码与(部分)银行帐号匹配的所有发票作为过滤器。该查询连接了也需要参数化的用户输入。这次,使用Spring框架组件JdbcTemplate来实现这一点。

4.1 task1

  • JdbcTemplate 使用位置参数来参数化查询。位置参数由 ? 表示。特点。将连接的变量替换为该字符。

  • 与上一步一样,使用 concat 函数参数化bankAccountFilter。

按照要求修改代码。

@Override    public List<Invoice> filterReceivedInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",                "SELECT i.* FROM invoice AS i",                "JOIN bank_account AS ba ON i.seller_id = ba.id AND i.buyer_id = ?",                "WHERE ba.account_number LIKE CONCAT('%', ?, '%')");        return jdbcTemplate.query(query, Invoice::fromRow, bankAccountId, bankAccountFilter);    }

*左右滑动查看更多

4.2 task2

  • 将 BankAccountId 和bankAccountFilter 作为参数添加到现有查询方法中。由于 jdbcTemplate 使用位置参数,因此它们的传递顺序很重要。

Step Solutionvar query = String.join(" ",        "SELECT i.* FROM invoice AS i",        "JOIN bank_account AS ba ON i.seller_id = ba.id AND i.buyer_id = ?",        "WHERE ba.account_number LIKE CONCAT('%',?,'%')");return jdbcTemplate.query(query, Invoice::fromRow, bankAccountId, bankAccountFilter);

*左右滑动查看更多

整理后得到:

@Override    public List<Invoice> filterReceivedInvoicesByAccountNumber(String bankAccountFilter, long bankAccountId) {        var query = String.join(" ",            "SELECT i.* FROM invoice AS i",            "JOIN bank_account AS ba ON i.seller_id = ba.id AND i.buyer_id = ?",            "WHERE ba.account_number LIKE CONCAT('%',?,'%')");    return jdbcTemplate.query(query, Invoice::fromRow, bankAccountId, bankAccountFilter);    }

*左右滑动查看更多

提交通过,分析一下:

该方法是重写了接口中的 filterReceivedInvoicesByAccountNumber 方法,用于根据银行账户过滤已接收的发票。

  • 使用 @Override 注解表示该方法是对接口方法的重写。

  • query 是用于构建 SQL 查询语句的字符串,使用 String.join 方法将多个字符串拼接为一个完整的查询语句。

  • 查询语句使用了 JOIN 来关联 invoice 表和 bank_account 表,并通过 seller_id 和 buyer_id 进行匹配。

  • WHERE 子句使用 LIKE 来模糊匹配银行账户号码。

  • jdbcTemplate.query 方法执行查询,并将结果转换为 Invoice 对象列表,使用 Invoice::fromRow 方法进行转换。

该方法通过 JdbcTemplate 执行 SQL 查询,根据指定的银行账户过滤条件,返回符合条件的已接收发票列表。

5、步骤三 使用生成的查询

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

本机查询现在是安全、整洁的,接下来继续下一步。Spring Data 可以根据方法名称中特定关键字的使用来派生查询。第一部分应该是动作动词,例如 find、read、get... 名称的其余部分是使用条件关键字和与查询实体相关的属性名称构造的条件。例如 getTransactionById。

5.1 task1

浏览至空接口 TransactionRepository.java。存储库需要执行 CRUD(创建、读取、更新、删除)操作,因此:

  • 使用 CrudRepository<T, Id> 扩展它。

1、其中第一个类型参数是数据库实体 Transaction。 

2、第二个是 ID 类型:Transaction 有一个 Long 作为 ID。

Task Solutionpublic interface TransactionRepository extends CrudRepository<Transaction, Long> {  // query method}

*左右滑动查看更多

TransactionRepository.javapackage vikingbank.web;public interface TransactionRepository {}

*左右滑动查看更多

按要求修改为以下代码:

TransactionRepository.java源码package vikingbank.web;import org.springframework.data.repository.CrudRepository;import jakarta.transaction.Transaction;public interface TransactionRepository extends CrudRepository<Transaction, Long> {  // query method}

*左右滑动查看更多

5.2 task2

在此接口内,创建一个返回事务列表的查询方法,其中:

  • 根据OwnerAccountId 查找交易。

  • 金额介于两个值之间。

  • 根据最新的时间戳对这些交易进行排序。

  • 此方法需要三个参数:id、金额的最小值和最大值。

Task SolutionList<Transaction> findByOwnerAccountIdAndAmountIsBetweenOrderByTimestampDesc(long id, double min, double max);

*左右滑动查看更多

Step Solutionimport java.util.List;import org.springframework.data.repository.CrudRepository;import vikingbank.web.entities.Transaction;public interface TransactionRepository extends CrudRepository<Transaction, Long> {    List<Transaction> findByOwnerAccountIdAndAmountIsBetweenOrderByTimestampDesc(long id, double min, double max);}

*左右滑动查看更多

按照要求修改一下:

TransactionRepository.javapackage vikingbank.web;import java.util.List;import org.springframework.data.repository.CrudRepository;import vikingbank.web.entities.Transaction;public interface TransactionRepository extends CrudRepository<Transaction, Long> {    /**     * 根据所属账户ID和金额范围进行查询,并按照时间戳降序排序     *     * @param id  所属账户ID     * @param min 最小金额     * @param max 最大金额     * @return 符合条件的交易列表     */    List<Transaction> findByOwnerAccountIdAndAmountIsBetweenOrderByTimestampDesc(long id, double min, double max);}

*左右滑动查看更多

提交通过,分析一下。

该接口继承自 CrudRepository 接口,用于操作 Transaction 实体对象的数据访问和持久化。

  • findByOwnerAccountIdAndAmountIsBetweenOrderByTimestampDesc 方法用于根据所属账户ID和金额范围进行查询,并按照时间戳降序排序。

  • 参数说明:

    • id:所属账户ID,用于匹配交易的所属账户ID。

    • min:最小金额,用于匹配交易的金额范围下限。

    • max:最大金额,用于匹配交易的金额范围上限。

  • 返回值:符合条件的交易列表。

该接口提供了通过所属账户ID和金额范围进行查询交易的功能,并按照时间戳降序排序返回结果。

九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

总结

在构建 SQL 查询时,避免使用未经验证的用户输入进行字符串拼接。即使在确保数据安全的情况下,也要始终避免在查询中使用拼接。应该使用参数化查询或使用特定框架的 API 来自动处理这一过程。

原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月16日23:55:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   九维团队-绿队(改进)| Java Spring编码安全系列之SQL注入http://cn-sec.com/archives/2151106.html

发表评论

匿名网友 填写信息