使用 JPA 和 Hibernate 防止 Java 中的 SQL 注入

1,191 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情

使用带有 Java Persistence API (JPA) 和 Hibernate ORM 的 SQL 参数防止 Java 中的 SQL 注入。

当我们查看 OWASP 的Top 10 漏洞时,SQL 注入仍然处于流行的位置。在这篇简短的文章中,我们将讨论如何避免 SQL 注入的几个选项。

当应用程序必须处理始终存在高安全性问题的数据库时,如果入侵者有可能劫持应用程序的数据库层,他可以在几个选项中进行选择。窃取存储用户的数据以向他们发送垃圾邮件并不是最糟糕的情况。更成问题的是当存储的支付信息被滥用。SQL注入网络攻击的另一种可能性是非法访问受限付费内容和/或服务。正如我们所见,关注(Web)应用程序安全性是十分必要的。

要找到针对 SQL 注入的有效预防措施,我们首先需要了解 SQL 注入攻击的工作原理以及我们需要注意哪些方面。简而言之:处理 SQL 查询中未经过滤的输入的每个用户交互都是可能的攻击目标。可以通过提交的 SQL 查询包含与原始查询不同的逻辑来操作数据输入。

示例1:简单的SQL注入

SELECT Username, Password, Role FROM User
   WHERE Username = 'John Doe' AND Password = 'S3cr3t';
SELECT Username, Password, Role FROM Users
   WHERE Username = 'John Doe'; --' AND Password='S3cr3t';

示例1 中的第一条语句显示了原始查询。如果变量用户名和密码的输入没有被过滤,我们就缺乏安全性。第二个查询使用用户名 John Doe 为变量 Username 注入一个字符串,并使用字符 ; -- 进行扩展。此语句绕过AND分支,并在本例中提供对登录的访问。';sequence关闭WHERE语句,并且 - 所有后续字符都未注释。理论上,可以在两个字符序列之间执行每个有效的 SQL 代码。

当然,我不是散布 SQL 命令可能会给受害者带来最坏后果的想法。通过这个简单的例子,我假设信息很清楚。我们需要保护应用程序中的每个 UI 输入变量免受用户操作。即使它们不直接用于数据库查询。为了检测这些变量,验证所有现有的输入表单总是一个好主意。但现代应用程序大多不仅仅是几个输入表单。出于这个原因,要密切关注 REST 端点。通常它们的参数也与 SQL 查询相关联。

因此,输入验证通常应该是安全概念的一部分。为此,来自 Bean Validation [2] 规范的注释非常强大。比如@NotNull,作为域对象中数据字段的Annotation,保证只有在变量不为空的情况下,对象才能持久化。要在 Java 项目中使用 Bean Validation Annotations,只需要包含一个小库。

示例2:Bean 验证的 Maven 依赖项

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>${version}</version>
</dependency>

有必要验证更复杂的数据结构。使用正则表达式,你将拥有另一个强大的工具。但需要注意的是,编写正确的工作正则表达式并不容易。

示例3:Java 中的正则表达式验证

public static final String RGB_COLOR = "#[0-9a-fA-F]{3,3}([0-9a-fA-F]{3,3})?";

public boolean validate(String content, String regEx) {
    boolean test;
    if (content.matches(regEx)) {
        test = true;
    } else {
        test = false;
    }
    return test;
}

validate('#000', RGB_COLOR);

检测正确 RGB 颜色模式的 RegEx 非常简单。有效输入为#ffF 或#000000。字符的范围是 0-9,字母 A 到 F。不区分大小写。当开发自己的 RegEx 时,总是需要检查非常好的现有边界。一个很好的例子是 24 小时时间格式。典型的错误是无效条目,如 23:60 或 24:00。validate 方法将输入字符串与 RegEx 进行比较。如果模式与输入匹配,该方法将返回 true。

我们保护用户输入免受滥用的第一个想法是过滤掉所有有问题的字符序列,比如 - 等等。创建阻止列表的意图并没有那么糟糕。但还是有一些限制。起初,应用程序的复杂性增加了,因为阻塞了单个字符,例如 –;和 ' 可能会导致副作用。此外,应用程序范围的字符默认限制有时可能会导致问题。

这意味着我们需要另一个强大的概念来以 SQL 查询无法操作的方式过滤输入。为了达到这个目标,SQL 标准有一个我们可以使用的非常好的解决方案。SQL 参数是 SQL 查询中的变量,将被解释为内容而不是语句。这允许大文本阻止一些危险字符。让我们看看这将如何在 PostgreSQL 数据库上工作。

示例 4:在 PostgreSQL 中定义参数

DECLARE user String;
SELECT * FROM login WHERE name = user;

在使用 OR 映射器 Hibernate 的情况下,Java Persistence API (JPA) 存在一种更适配的方式。

示例 5:Hibernate JPA SQL 参数用法

String myUserInput;

@PersistenceContext
public EntityManager mainEntityManagerFactory;

CriteriaBuilder builder =
    mainEntityManagerFactory.getCriteriaBuilder();

CriteriaQuery<DomainObject> query =
    builder.createQuery(DomainObject.class);

// create Criteria
Root<ConfigurationDO> root =
    query.from(DomainObject.class);

//Criteria SQL Parameters
ParameterExpression<String> paramKey =
    builder.parameter(String.class);

query.where(builder.equal(root.get("name"), paramKey);

// wire queries together with parameters
TypedQuery<ConfigurationDO> result =
    mainEntityManagerFactory.createQuery(query);

result.setParameter(paramKey, myUserInput);
DomainObject entry = result.getSingleResult();

示例5 显示为使用 JPA 和条件 API 的 Hibernate 的完整示例。用户输入的变量在第一行声明。示例中的注释解释了它的工作方式。除了提高 Web 应用程序的安全性之外,该解决方案还有其他一些好处。没有使用纯 SQL,这确保了 Hibernate 支持的每个数据库管理系统都可以通过此代码得到保护。使用起来确实比一般的查询要复杂一些,但是对你应用程序的好处是巨大的。另一方面,还有一些额外的代码行,它们并不难理解。