首页 > 世链号 > 10 个你必须知道的 Java 安全最佳实践
币圈印象  

10 个你必须知道的 Java 安全最佳实践

摘要:在 2017 版 OWASP 十大漏洞中,注入攻击在当年名列前茅。查看典型的 Java SQL 注入,会发现查询参数拼接进了 SQL 语句。下面 Java 代码执行的 SQL 非常不安全,攻击者会利用它来获取设定之外的信息。

编译:ImportNew/ 唐尤华

snyk.io/blog/10-java-security-best-practices/

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

在 2017 版 OWASP 十大漏洞中,注入攻击在当年名列前茅。查看典型的 Java SQL 注入,会发现查询参数拼接进了 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 程序中使用 PreparedStatement 对查询参数化。这应该成为创建数据库查询的唯一方法。通过定义完整的 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 代码。

2. 使用带双因子验证的 OpenID Connect

身份管理与访问控制是非常困难的,而身份验证失败通常是造成数据泄露的主要原因。实际上,这个问题在 OWASP 十大漏洞列表中排名第二。自己进行身份验证时需要考虑很多因素:密码安全存储、强加密、凭证检索等。很多时候,使用类似 OpenID Connect 这样的解决方案更安全更简单。OpenID Connect (OIDC)可以实现跨网站和应用程序用户身份验证。这样不再需要保存和管理密码文件。OpenID Connect 是一个 OAuth 2,0 扩展,可以提供用户信息。除访问令牌外,还提供了一个 ID 令牌和 /userinfo 端点,可以获得更多信息。它还提供了端点发现和客户端动态注册功能。

用 Spring Security 设置 OpenID Connect 很简单。请确保您的应用程序强制执行 2FA (双因子身份验证)或 MFA (多因素身份验证),在系统中提供额外的安全层。

向 Spring Boot 应程程序添加 oauth2-client 和 Spring Security 依赖可以利用 Google、Github 和 Okta 等第三方客户端处理 OIDC。创建应用程序后,只需在应用程序配置中根据需要像下面这样指定客户端,可以是 GitHub 或者 Okta client-id 与 client-secret。

pom.xml

 org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-security 

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. 扫描依赖项查找已知漏洞

您很有可能不知道自己的应用程序究竟使用了多少个直接依赖项,也极有可能不知道应用程序包含了多少个可传递依赖项。尽管依赖项构成了整个应用程序的绝大部分,但实际的结果通常如此。攻击者越来越多地将目标锁定在开源依赖项上,使用它们会成为恶意攻击的受害者。确保应用程序的整个依赖树中没有已知漏洞很重要。

Snyk 可以测试应用程序生成的构件,把那些有漏洞的依赖项标记出来。Snyk 会在仪表盘中展示软件包存在的漏洞列表。

10 个你必须知道的 Java 安全最佳实践

此外,通过对代码仓库 pull request,还会给出升级建议或者补丁程序补救安全漏洞。Snyk 使用 WebHook 对 pull request 执行自动测试,确保不会引入新的已知漏洞。

Snyk 支持 Web 和 CLI,可以集成到 CI 流程。经过配置,当漏洞的严重性超过设置的阈值时能中断构建。

开源项目或者每个月测试次数不多的私人项目可以免费使用 Snyk。

4. 处理敏感数据要小心

暴露敏感数据会带来风险,比如客户的个人数据或者信用卡号。一些注意不到的细节同样有风险,例如在系统中公开唯一标识符,该标识符可以用来在其他调用中获取额外的数据。

首先,仔细查看程序设计确认是否真的需要这些数据。最重要的是确保敏感数据不被泄露,包括日志、自动完补全、数据传输等。

要防止日志泄露敏感数据,一种简单的方法是清理域实体中的 toString() 方法。这样可以避免意外打印出敏感字段。如果项目使用 Lombok 生成 toString() 方法,可以用 @ToString.Exclude 把字段排除在 toString() 输出之外。

另外,为外部提供数据时也要非常小心。例如:如果系统中的某个端点可以显示所有用户名,那么不要提供系统内部唯一标识符。唯一标识符可能被用来获取用户其他敏感信息。如果使用 Jackson 实现 POJO 的 JSON 序列化和反序列化,可以用 @JsonIgnore 和 @JsonIgnoreProperties 阻止这些属性被序列化或反序列化。

如果需要把敏感数据发送到其他服务,请对其进行适当的加密,并确保使用 HTTPS 保护连接安全。

5. 清理所有输入

跨站点脚本攻击(XSS)是一个众所周知的问题,通常出现在 Javascript 应用程序中。然而,Java 也不能幸免。XSS 只是注入远程执行的 Javascript 代码。根据 OWASP 的说法,预防 XSS 的#0 号规则是“不要在允许的位置插入非信任数据”。要解决这个问题,最基本的方法是在使用数据之前,尽可能地预防不可信数据并清除所有其他内容。OWASP Java encoding 是一个很好的选择,提供了多种 encoder。

 org.owasp.encoder encoder 1.2.2 
 String untrusted = ""; System.out.println(Encode.forHtml(untrusted)); // output: 

对用户输入的文本进行处理是必须的。但如果是从数据库检索出的数据呢?假如数据库遭到破坏,有人在数据库字段或文档中植入了一些恶意文本该怎么办?

另外,对传入的文件也要注意。Zip-slip 漏洞在很多库中都存在,原因就是没有对 zip 文件路径进行检查。Zip 包含的文件可能带有 ../../../../foo.xy 这样的名字,在解压缩时可能会覆盖任意文件。尽管这不是 XSS 攻击,但是这个例子充分说明了为什么必须清理所有输入。每个输入都可能是恶意的,因此都要进行清理。

6. 配置 XML 解析器防止 XXE

启用 XML 外部实体(XXE)后,可能被利用创建恶意 XML,像下面这样读取计算机上任意文件的内容。XXE 攻击是 OWASP 排名前十的攻击之一。Java XML 库特别容易受到 XXE 注入,因为大多数 XML 解析器默认启用了外部实体。

 "1.0" encoding="UTF-8" standalone="yes"?> "file:///etc/passwd">]> &xxe; Bohemian Rhapsody A Night at the Opera 

下面的 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)); } }; 

更改默认设置,禁止 xerces1 或 xerces2 的外部实体和 doctype 可防止此类攻击。

 ... 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 备忘。

7. 避免 Java 序列化

Java 序列化可以把对象转换为字节流。转换后的字节流可以存到磁盘上或者传给其他系统。相反的过程称为反序列化,可以从字节流重新创建原始对象。

最大的问题在于反序列化,通常看起来像下面这样:

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

在解码之前,无法知道反序列化的内容。攻击者可能会构建恶意对象,序列化以后发送给应用程序。一旦调用 readObject(),恶意对象就会实例化。您可能认为这些攻击不可能发生,因为这意味着 classpath 上会出现易受攻击的类。但如果考虑 classpath 上类的数量,包括自己的代码、Java 库、第三方库和框架,很有可能出现易受攻击的类。

由于这些年来出现了很多问题,Java 序列化也被戏称为“送礼送不停”。Oracle 计划把 Java 序列化作为 Project Amber 的一部分删除。但这可能需要一些时间,而且不太可能在之前的版本中解决。因此,明智的做法是尽可能避免 Java 序列化。如果需要在域实体上实现可序列化,最好自己实现 readObject(),像下面这样。可以防止反序列化。

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

如果需要对输入流自己进行反序列化,应该使用 ObjectsInputStream 并进行限制。一个很好的例子是 Apache Commons IO 的 ValidatingObjectInputStream。这个 ObjectInputStream 会查对象是否允许反序列化。

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

对象反序列化问题不仅限于 Java 序列化。从 JSON 反序列化为 Java 对象也有类似的问题。

8. 使用强加密和哈希算法

要在系统中存储敏感数据,必须确保加密。首先需要选择加密类型,比如对称加密或者非对称加密。另外需要确认安全性做到何种程度。加密越强花费的时间越多,消耗 CPU 资源也越多。最重要的是不必自己实现加密算法。加密是一项很难的活,可以选择合适的加密开发库解决。

例如,如果要加密信用卡详细信息之类的内容,可能需要对称算法,因为需要能够获取原始号码。假如使用高级加密标准(AES),该标准目前是美国联邦组织的标准对称加密算法。要完成加密和解密工作,没有理由深入研究底层 Java 加密技术。建议使用开发库完成繁重的工作。例如 Google Tink。

 com.google.crypto.tink tink 1.3.0-rc1 

下面是一个简短的示例,展示了如何使用带有 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); } 

密码使用非对称加密比较安全,因为不需要检索原始密码,只要哈希匹配即可。BCrypt 配合 SCrypt 可以很好地完成任务。两者都采用密码散列(单向函数),计算复杂,需要消耗大量计算时间。这正是你想要的,因为蛮力攻击需要很长时间。

Spring security 为各种算法提供了出色的支持。可以使用 Spring Security tool 5 提供的 SCryptPasswordEncoder 和 BCryptPasswordEncoder 对密码进行哈希。

今天的强加密算法一年后可能会变成弱加密算法。因此,需要定期检查确保使用正确的算法。使用经过审查的安全开发库并保持最新。

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 官方文档。

10. 集中记录日志和监控

安全不仅仅是预防,还要知道问题发生的时间,以便采取相应的措施。使用哪个日志库并不重要。OWASP Top 10 指出,重要的是记录很多日志。日志记录不足是一个大问题。通常,所有可供审核的事件都应该记录。像异常、登录和登录失败这样的事件都需要记录,还可能希望记录每个传入请求及其来源。万一被黑客入侵,至少能够知道发生了什么、发生的具体时间和方式。

建议集中记录日志。例如,如果使用 logback 或 log4j,可以很容易连上集中式日志分析平台 Elastic Stack。使用像 Kibana 这样的工具,可以访问和搜索来自所有服务器和系统的日志进行调查。

仅次于记录日志,还应当主动监视系统并把这些内容集中存储到易于访问的地方。出现像 CPU 峰值或者单个 IP 地址负载过高的情况,很可能表明出现了问题或者攻击。把集中式日志记录和实时监视与警报结合在起来,可以在出现情况时及时收到通知,比如管理员密码重置、内部服务器受到外部 IP 访问、URL 参数包含‘UNION’等这样不正常的情形。收到类似的问题警报时,及时跟踪发生的情况能够防止遭受更大的损失并能及时修复漏洞。

Tags:
免责声明
世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。