10 个 Java 安全最佳实践

Java 安全问题

尽管我们都致力于编写出色的代码,但 Java 安全性并不总是开发人员思维的一部分。但是,防止 Java 安全问题应该与使您的 Java 应用程序具有高性能、可扩展性和可维护性一样重要。

您还应该注意 Java 中的新漏洞,例如Log4Shell (CVE-2021-44228),这些漏洞于 2021 年 12 月披露,影响运行 Apache Log4j 易受攻击版本的应用程序。


在这份备忘单中,我们讨论了 10 个常见的 Java 安全问题。我们将为您提供一些实用的指导和示例,说明如何在您编写的应用程序中防止这些常见的 Java 安全漏洞。

1.使用查询参数化防止注入

在 2017 年版本的OWASP Top 10 漏洞中,注入成为当年排名第一的漏洞。在查看 Java 中的典型SQL 注入时,后续查询的参数天真地连接到查询的静态部分。以下是 Java 中 SQL 的不安全执行,攻击者可以使用它来获取比预期更多的信息。

public void selectExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = " + parameter;
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   printResult(result);
}

如果此示例中的参数类似于,则结果包含表中的每一项。如果数据库支持多个查询并且参数是. '' OR 1=1''; UPDATE USERS SET lastname=''

为了防止这种 Java 安全风险,我们应该使用准备好的语句来参数化查询。这应该是创建数据库查询的唯一方法。通过定义完整的 SQL 代码并稍后将参数传递给查询,代码更容易理解。最重要的是,通过区分SQL代码和参数数据,查询不会被恶意输入劫持。  

public void prepStatmentExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = ?";
   PreparedStatement statement = connection.prepareStatement(query);
   statement.setString(1, parameter);
   System.out.println(statement);
   ResultSet result = statement.executeQuery();

   printResult(result);
}

在上面的示例中,输入绑定到类型String,因此是查询代码的一部分。此技术可防止参数输入干扰 SQL 代码。 

有关 SQL 注入预防的更多信息,请查看此方便的指南:SQL 注入备忘单:防止 SQL 注入攻击的 8 个最佳实践

2. 使用 OpenID Connect 和 2FA

身份管理和访问控制很困难,身份验证被破坏通常是数据泄露的原因。事实上,这是OWASP 前 10 个漏洞列表中的第 2 位,因此是主要的 Java 安全风险。在自己创建身份验证时,您应该考虑很多事情:密码的安全存储、强加密、凭据的检索等。在许多情况下,使用OpenID Connect等令人兴奋的解决方案会更容易、更安全。OpenID Connect (OIDC) 使您能够跨网站和应用程序对用户进行身份验证。这消除了拥有和管理密码文件的需要。 OpenID Connect 是一个提供用户信息的OAuth 2.0扩展。除了访问令牌之外,它还添加了一个 ID 令牌,以及一个/userinfo获取更多信息的端点。它还添加了端点发现功能和动态客户端注册。

使用Spring Security等库设置 OpenID Connect是一项简单而常见的任务。确保您的应用程序强制执行 2FA(双因素身份验证)或 MFA(多因素身份验证)以在您的系统中添加额外的安全层。

通过将 oauth2-client 和 Spring 安全依赖项添加到Spring Boot应用程序,您可以利用 Google、Github 和Okta等第三方客户端来处理 OIDC。创建应用程序后,您只需通过在应用程序配置中指定它来将其连接到您选择的特定客户端;这可能是您的 GitHub 或 Okta 客户端 ID 和客户端密钥,如下所示。

扫描二维码关注公众号,回复: 14442263 查看本文章

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

application.yaml

spring:
 security:
   oauth2:
     client:
         registration:
           github:
             client-id: 796b0e5403be4729ca01
             client-secret: f379318daa27502254a05e054361074180b840a9
           okta:
             client-id: 0oa1a4wascEpYu6yk358
             client-secret: hqxj7a9lVe_TudbS2boBW7AWwxTlZiHNrJxdc_Sk
             client-name: Okta
         provider:
           okta:
             issuer-uri: https://dev-844689.okta.com/oauth2/default

3. 扫描您的依赖项以查找已知漏洞

您很有可能不知道您的应用程序使用了多少直接依赖项。您也极有可能不知道您的应用程序使用了多少传递依赖项。这通常是正确的,尽管依赖项构成了整个应用程序的大部分。攻击者越来越多地针对开源依赖项,因为它们的重用为恶意攻击者提供了许多受害者。确保应用程序的整个依赖关系树中没有已知的 Java 安全漏洞非常重要。

Snyk 测试您的应用程序构建工件,标记那些具有已知漏洞的依赖项。它为您提供了一个 Java 安全漏洞列表,这些漏洞存在于您在应用程序中用作仪表板的包中。

此外,它通过针对您的源代码存储库的拉取请求建议升级版本或提供补丁来修复您的安全问题。Snyk 还通过确保对您的存储库提出的任何未来拉取请求进行自动测试(通过 webhook)来保护您的环境,以确保它们不会引入新的已知漏洞。

Snyk 可通过 Web UI 和 CLI 使用,因此您可以将其与 CI 环境集成并对其进行配置,以在存在严重性超出设置阈值的漏洞时中断构建。

免费为开源项目或每月测试次数有限的私人项目使用Snyk。

4. 小心处理敏感数据

暴露敏感数据(例如客户的个人数据或信用卡号)可能是有害的。但即使是比这更微妙的情况也可能同样有害。例如,如果该标识符可以在另一个调用中用于检索附加数据,那么在您的系统中暴露唯一标识符就是一个 Java 安全漏洞。 

首先,您需要仔细查看应用程序的设计并确定您是否真的需要这些数据。最重要的是,确保您不暴露敏感数据,可能是通过日志记录、自动完成、传输数据等。 

防止敏感数据出现在日志中的一种简单方法是清理域实体的方法。这样您就不会意外打印敏感字段。如果您使用项目 Lombok 来生成您的方法,请尝试使用来防止字段成为输出的一部分。 toString()toString()@ToString.ExcludetoString()

此外,在将数据暴露给外界时要非常小心。例如:如果我们在系统中有一个显示所有用户名的端点,则不需要显示内部唯一标识符。此唯一标识符可用于通过使用其他端点将其他更敏感的信息连接到用户。如果您使用 Jackson 将 POJO 序列化和反序列化为 JSON,请尝试使用  @JsonIgnore@JsonIgnoreProperties防止这些属性被序列化或反序列化。

如果您需要将敏感数据发送到其他服务,请正确加密并确保您的连接使用 HTTPS 保护。

5.清理所有输入

跨站点脚本 (XSS) 是一个众所周知的问题,主要用于 JavaScript 应用程序。然而,Java 也不能幸免。XSS 只不过是远程执行的 JavaScript 代码注入。根据 OWASP,用于防止 XSS 的规则 #0 是“永远不要插入不受信任的数据,除非在允许的位置”。这种 Java 安全风险的基本解决方案是尽可能地防止不受信任的数据,并在使用数据之前清理其他所有内容。一个很好的起点是 OWASP Java 编码库,它为您提供了许多编码器。

<dependency>
   <groupId>org.owasp.encoder</groupId>
   <artifactId>encoder</artifactId>
   <version>1.2.2</version>
</dependency>
String untrusted = "<script> alert(1); </script>";
System.out.println(Encode.forHtml(untrusted));

// output: <script> alert(1); </script>

清理用户文本输入是显而易见的。但是您从数据库中检索的数据呢,即使它是您自己的数据库?如果您的数据库遭到破坏并且有人在数据库字段或文档中植入了一些恶意文本怎么办? 

另外,请留意传入的文件。许多库中存在Zip-slip漏洞是因为未对压缩文件的路径进行清理。可以提取包含带有路径的文件的 Zip 文件,并可能覆盖任意文件。虽然这不是 XSS 攻击,但它是一个很好的例子,说明了为什么你必须清理所有输入。每个输入都可能是恶意的,应该进行相应的清理。../../../../foo.xy

6. 配置您的 XML 解析器以防止 XXE

启用 XML 外部实体 (XXE) 后,可以创建如下所示的恶意 XML,并读取机器上任意文件的内容。XXE 攻击是 OWASP 前 10 名列表的一部分,并且是我们需要防止的 Java 安全漏洞,这并不奇怪。Java XML 库特别容易受到 XXE 注入的影响,因为大多数 XML 解析器默认启用了外部实体。  

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE bar [
       <!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<song>
   <artist>&xxe;</artist>
   <title>Bohemian Rhapsody</title>
   <album>A Night at the Opera</album>
</song>

DefaultHandler 和 Java SAX 解析器的简单实现(如下所示)解析此 XML 文件并显示 passwd 文件的内容。Java SAX 解析器案例在这里用作主要示例,但其他解析器,如 DocumentBuilder 和 DOM4J,具有类似的默认行为。

SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

DefaultHandler handler = new DefaultHandler() {

    public void startElement(String uri, String localName,String qName,Attributes attributes) throws SAXException {
        System.out.println(qName);
    }

    public void characters(char ch[], int start, int length) throws SAXException {
        System.out.println(new String(ch, start, length));
    }
};

更改默认设置以分别禁止xerces1xerces2的外部实体和文档类型,以防止此类攻击。

...
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

factory.setFeature("https://xml.org/sax/features/external-general-entities", false);
saxParser.getXMLReader().setFeature("https://xml.org/sax/features/external-general-entities", false);
factory.setFeature("https://apache.org/xml/features/disallow-doctype-decl", true); 
... 

有关防止恶意 XXE 注入的更多实践信息,请查看OWASP XXE 备忘单。或者查看我关于如何防止外部实体 (XXE) 注入攻击的视频。

7.避免Java序列化

Java 中的序列化允许我们将对象转换为字节流。此字节流要么保存到磁盘,要么传输到另一个系统。反过来,字节流可以被反序列化并允许我们重新创建原始对象。

最大的问题是反序列化部分。通常它看起来像这样:

ObjectInputStream in = new ObjectInputStream( inputStream );
return (Data)in.readObject();

在解码之前无法知道要反序列化的内容。攻击者可能会序列化恶意对象并将其发送到您的应用程序。一旦你调用,恶意对象就已经被实例化了。您可能认为此类攻击是不可能的,因为您的类路径中需要有一个易受攻击的类。但是,如果考虑类路径上的类数量——包括你自己的代码、Java 库、第三方库和框架——很可能存在一个易受攻击的类。  readObject()

Java 序列化也被称为“不断给予的礼物”,因为它多年来产生了许多问题。Oracle 计划最终删除作为Project Amber的一部分的 Java 序列化。但是,这可能需要一段时间,并且不太可能在以前的版本中修复。因此,明智的做法是尽可能避免 Java 序列化。如果您需要在域实体上实现可序列化,最好实现它自己的,如下所示。这可以防止反序列化。readObject()

private final void readObject(ObjectInputStream in) throws java.io.IOException {
   throw new java.io.IOException("Deserialized not allowed");
}

如果您需要自己反序列化输入流,则应使用ObjectsInputStreamwith 限制。一个很好的例子是ValidatingObjectInputStream来自 Apache Commons IO。这将ObjectInputStream检查反序列化的对象是否被允许。

FileInputStream fileInput = new FileInputStream(fileName);
ValidatingObjectInputStream in = new ValidatingObjectInputStream(fileInput);
in.accept(Foo.class);

Foo foo_ = (Foo) in.readObject();

对象反序列化问题不仅限于 Java 序列化。从 JSON 到 Java 对象的反序列化可能包含类似的问题。Jackson 库的此类反序列化问题的一个示例是在博客文章“ Jackson Deserialization Vulnerability ”中

要了解更多信息,请查看我们的 Java 中的序列化和反序列化:解释 Java 反序列化漏洞博客。

8. 使用强加密和散列算法。

如果您需要在系统中存储敏感数据,则必须确保已进行适当的加密。首先你需要决定你需要什么样的加密——例如,对称或非对称。此外,您需要选择它需要的安全性。更强的加密需要更多时间并消耗更多 CPU。最重要的部分是您不需要自己实现加密算法。加密很困难,受信任的库可以为您解决加密问题。

例如,如果我们想加密信用卡详细信息之类的东西,我们可能需要一个对称算法,因为我们需要能够检索原始号码。假设我们使用高级加密标准 (AES),它目前是美国联邦组织的标准对称加密算法。要加密和解密,没有理由深入研究低级 Java 加密。我们建议您使用可以为您完成繁重工作的库。例如,谷歌叮当

<dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>tink</artifactId>
   <version>1.3.0-rc1</version>
</dependency>

下面是一个简短示例,说明如何使用 AES 和关联数据 (AEAD) 进行身份验证加密。这使我们能够加密明文并提供应经过身份验证但未加密的关联数据。

private void run() throws GeneralSecurityException {
   AeadConfig.register();
   KeysetHandle keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM);

   String plaintext = "I want to break free!";
   String aad = "Queen";

   Aead aead = keysetHandle.getPrimitive(Aead.class);
   byte[] ciphertext = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encr = Base64.getEncoder().encodeToString(ciphertext);
   System.out.println(encr);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encr), aad.getBytes());
   String decr = new String(decrypted);
   System.out.println(decr);
}

对于密码,使用强加密散列算法更安全,因为我们不需要检索原始密码,只需匹配散列即可。根据OWASP Password cheat Shee t,目前最好的密码散列算法是Argon2BCrypt。对于遗留系统Scrypt可以在一定程度上使用。
这三个都是密码散列(单向函数)和计算困难的算法,会消耗大量时间。这正是您想要的,因为蛮力攻击以这种方式需要很长时间。

Spring 安全性为各种算法提供了出色的支持。尝试使用Spring Security 5.0 为密码散列目的提供的 和Argon2PasswordEncoderBCryptPasswordEncoder

今天是强加密算法,一年后可能是弱算法。因此,需要定期检查加密,以确保您使用正确的算法来完成这项工作。使用经过审查的安全库来完成这些任务,并使您的库保持最新。

9. 启用 Java 安全管理器

默认情况下,Java 进程没有任何限制。它可以访问各种资源,例如文件系统、网络、外部进程等等。但是,有一种机制可以控制所有这些权限,即 Java 安全管理器。默认情况下,Java 安全管理器处于非活动状态,并且 JVM 对机器具有无限的权力。尽管我们可能不希望 JVM 访问系统的某些部分,但它确实可以访问。更重要的是,Java 提供了可以做令人讨厌和意想不到的事情的 API。

我认为最可怕的是 Attach API。使用此 API,您可以连接到其他正在运行的 JVM 并控制它们。例如,如果您可以访问机器,那么更改正在运行的 JVM 的字节码非常容易。Nicolas Frankel 的这篇博客文章举例说明了如何做到这一点。

激活 Java 安全管理器很容易。通过使用额外参数启动 Java,您可以使用默认策略激活安全管理器。java -Djava.security.manager

但是,默认策略可能不完全适合您系统的用途。您可能需要创建自己的自定义策略并将其提供给 JVM。java -Djava.security.manager -Djava.security.policy==/foo/bar/custom.policy

请注意双等号-这将替换默认策略。使用单个等号,使用您的自定义策略扩展默认策略。

有关 JDK 中的权限以及如何编写策略文件的更多信息,请查看官方 Java 文档

注意:自 Java 17 发布以来,由于JEP 415的实现,安全管理器被标记为“已弃用” 。尽管如此,它在 Java 17 中仍然可以完全发挥作用。目前,大多数 Java 开发人员在生产环境中使用 Java 8 或 Java 11。这意味着在实践中,即使将来会删除安全管理器,它仍然是一个很好的机制。

10.集中记录和监控

安全不仅仅是预防。您还需要能够检测何时出现问题,以便您可以采取相应的行动。您使用哪个日志库并不重要。根据 OWASP Top 10,重要的部分是你记录了很多,因为记录不足仍然是一个大问题。一般来说,应该记录所有可能是可审计事件的事件。异常、登录和登录失败之类的事情可能很明显,但您可能希望记录每个传入的请求,包括其来源。至少您知道发生了什么、何时以及如何发生的事情,以防您被黑客入侵。 

建议您使用一种机制来集中日志记录。例如,如果您使用 Log4j 或 logback,则很容易将其连接到集中式 Elastic Stack。使用 Kibana 等工具,可以访问和搜索来自所有服务器或系统的所有日志以进行调查。

除了记录之外,您还应该主动监控您的系统并将这些值集中存储并且易于访问。诸如 CPU 峰值或来自单个 IP 地址的巨大负载之类的事情可能表明存在问题或攻击。将集中式日志记录和实时监控与警报相结合,以便在发生奇怪事件时收到通知。 

诸如管理员密码重置、外部 IP 访问内部服务器或 URL 参数之类‘UNION’的想法只是表明某些事情不正常的几个指标。当您收到有关此类问题的适当警报并追溯实际发生的情况时,您很有可能设法防止任何更大的损坏并及时修复泄漏。

Faq

什么是 Java 安全性?

Java 安全性是指 Java 开发人员为防止恶意用户破坏应用程序而采取的措施。通过编写强大且安全的 Java 代码,开发人员可以防止应用程序和数据的机密性、完整性和可用性受到损害。

Java 是否存在安全风险?

Java 不一定是安全风险。如果您正确升级 Java,只使用所需的模块,并确保您的应用程序是按照安全思维构建的,那么您就可以将风险降到最低。此备忘单可帮助您防止您构建的应用程序中存在 Java 安全漏洞。

Java 安全文件位于何处?

该文件是 Java 运行时环境 (JRE) 中包含默认安全属性的文件。使用 Java 8 或更低版本时,可以在 $JAVA_HOME/jre/lib/security 中找到该文件。在较新的 Java 版本中,该文件位于.java.security$JAVA_HOME/conf/security

猜你喜欢

转载自blog.csdn.net/allway2/article/details/126179604