Java安全编程需要考虑的问题

这篇文章简要讨论了Java安全编程需要考虑的若干问题,通过对这些问题的深入理解,能够帮助我们在实际编码过程中避免出现安全相关的问题,从而提高代码质量。

由于时间关系,没有给出每个场景的示例代码,仅说明了该场景可能出现的安全问题以及对应的解决办法。


概述

一般而言,安全编程的目标有以下三点:

  • 机密性
  • 完整性
  • 可用性

机密性要求数据不被他人轻易获取,需要进行数据加密。

完整性要求数据不被他人随意修改,需要进行指纹计算。

可用性要求服务不被他人恶意攻击,需要进行数据校验。

在Java中,安全编程需要考虑以下场景:

  1. 数据校验
  2. 敏感信息保护
  3. 线程同步
  4. IO操作
  5. 反序列化
  6. Java平台安全机制与组件
  7. 其它安全问题

数据校验

任何数据都有信任域,在信任域中进行交互的数据都是可信数据,跨信任域交互的数据为不可信数据。对于任何的不可信数据,在处理时都要进行安全校验。

常见的不可信数据有:

  1. 配置文件
  2. 注册表参数
  3. 网络数据
  4. 环境变量
  5. 命令行参数
  6. 用户输入数据
  7. 进程间通信数据
  8. 外部函数输入参数
  9. 全局变量

在处理数据时一般的步骤为:

数据校验相关的安全问题有以下几点:

  1. SQL注入
  2. OS命令行注入
  3. XML注入
  4. 正则注入
  5. 日志注入
  6. 目录遍历攻击
  7. Zip解压攻击

在校验阶段我们要充分考虑到上面的这些安全问题,避免不可信数据进入到操作数据阶段。

SQL注入

SQL注入一般发生在拼接SQL的场景,例如,用户输入了下面的字符串:Jack ' or '1' = '1,服务器后端直接使用该字符串来拼接SQL以进行登录时的用户名和密码验证。

防御办法:

  1. Java中使用PreparedStatement预处理SQL
  2. 白名单/黑名单验证
  3. 特殊字符转码

在Hibernate等ORM框架中如果拼接SQL同样有SQL注入风险,特别的,在MyBatis中,只能使用形如#username#的方式拼接SQL,不能使用$username$的方式。

在MSSQL存储过程中如果使用EXEC()函数执行拼接后的SQL同样有SQL注入问题,应该使用sp_executesql通过传入参数的方式来执行SQL,传入参数可以达到预处理一样的效果。

OS命令行注入

OS命令行注入一般发生在拼接命令行参数的场景。例如,执行下面的命令行语句:"cmd.exe /c dir " + dir,其中,dir通过用户输入得到。如果用户输入的dir为:"C: & del C:\dbms\*.*" ,则出现了命令行注入问题。

防御办法:

  1. 尽量使用JDK中提供的API进行IO操作,而不是执行命令行
  2. 白名单/黑名单验证
  3. 特殊字符转码

XML注入

XML注入包含多种场景,下面分别讨论。

使用不可信数据修改XML

使用不可信数据修改XML,如果该数据中包含了<>"!--等特殊字符,则可能造成恶意增加节点等问题。

防御办法:

  1. 白名单/黑名单验证
  2. 使用安全的XML库来修改XML,如dom4j等
  3. 特殊字符转码

XML外部实体注入

XML External Entity(XXE)注入,攻击者可以很容易获取Server端资源、探测内部网络、引用无限文件产生DOS等。例如:

<!DOCTYPE xxx... [
    <!ENTITY xxe SYSTEM "file://etc/passwd">
]>
<root>&xxe;</root>

防御办法:

  1. 禁止包含实体,即XML中不能有DOCTYPE
  2. 禁止使用外部实体和参数实体
  3. 覆写解析器解析实体的方法
  4. 对实体内容校验

XML内部实体扩展攻击

XML Entity Expansion攻击,原理是实体自己引用自己,使得引用链呈几何增长,服务器后端无法及时解析XML,最终导致DOS。例如:

<!DOCTYPE a1 [
    <!ENTITY a1 "a1">
    <!ENTITY a2 "&a1";"&a1";"&a1";"&a1";"&a1";"&a1";"&a1";>
    <!ENTITY a3 "&a2";"&a2";"&a2";"&a2";"&a2";"&a2";"&a2";>
    <!ENTITY a4 "&a3";"&a3";"&a3";"&a3";"&a3";"&a3";"&a3";>
...
]>

防御办法:

  1. 禁止包含实体,即XML中不能有DOCTYPE
  2. 限制实体使用个数

使用不安全的XSLT转换XML

XSLT(Extensible Stylesheet Language Transformations)可以将XML转换成其它格式,如HTML、纯文本等,使用不安全的XSLT转换XML时可能会导致任意代码的执行。

防御办法:

  1. Java中可以使用TransformerFactory的安全策略防护
  2. 设置黑名单,禁用不安全的方法

正则注入

如果正则表达式是通过用户输入或其它不可信数据生成,那么可能出现正则注入问题。正则注入会导致ReDOS攻击,使服务器无法及时给出响应。包含两种场景:

  • 正则表达式包含具有自我重复的重复性分组,例如:
^(\d+)+$
^(\d*)*$
^(\d+)*$
^(\d+|\s+)*$
  • 正则表达式包含替换的重复性分组,例如:
^(\d|\d|\d)+$
^(\d|\d?)+$

防御办法:

  1. 限制正则表达式长度
  2. 不要使用复杂的正则表达式,少使用分组
  3. 避免动态构造正则表达式,如果必须动态构造,则需要进行白名单/黑名单验证
  4. 使用Possessive Match提高性能,去掉所有排列

日志注入

日志注入包含两个方面,日志伪造和敏感数据泄露。

日志伪造

外部输入的数据包含伪造的一些日志信息,当这些数据被记录到日志中,管理员在进行日志分析时会误以为真的出现了伪造的行为。

防御办法:

  1. 禁止输入\r\n等特殊字符
  2. 输入数据长度限制
  3. 特殊字符转码

敏感数据泄露

日志中包含了一些敏感数据,如密码、信用卡号、手机号码、身份证号等。在进行日志分析时可能会泄露这些敏感的信息。

防御办法:

  1. 日志中避免记录任意的外部输入
  2. 敏感信息在记录前用固定长度的*号代替

目录遍历攻击

如果用户输入的目录参数中包含了上一层目录符号,如..\..\..\../../../等,服务器后端在处理这些目录时可能会出现目录遍历攻击,使得攻击者能够非法下载或非法访问到没有权限的文件。

防御办法:

  1. 对文件路径进行标准化处理,即在Java中使用方法getCanonicalPath()获取目录绝对路径,然后判断该路径是否在合法的白名单路径中

注意:不能使用getAbsolutePath()方法获取目录绝对路径,因为该方法获取到的可能是软链接路径,getCanonicalPath()会处理掉软链接,获取真正的文件路径。

Zip解压攻击

在Java中,我们一般使用java.util.ZipInputStream来解压Zip文件,Zip解压攻击包含下面两个问题:

  1. Zip中包含的文件被解压到了目标路径以外
  2. 解压过程消耗了太多的系统资源,导致服务器宕机

防御办法:

  1. 对Zip中包含的文件进行路径校验,只解压路径在白名单中的文件
  2. 边解压边读取实际解压后的总文件大小及文件数量,如果解压后的总文件大小或数量超过限制,则停止解压,抛出异常。这样能防止Zip炸弹攻击

注意:不能通过读取Zip文件本身的某个属性来判断解压后总文件的大小,因为该属性可能被恶意修改。


敏感信息保护

敏感信息保护的目标是保护敏感信息不被篡改和泄露,敏感信息可包含密码、信用卡号、身份证号等。在以下场景中可能会出现敏感信息篡改和泄露:

  1. 不正确地抛出异常
  2. 序列化敏感信息

不正确地抛出异常

在程序中抛出异常时可能会泄露敏感信息,有以下场景:

  1. 用户请求目录不存在,抛出原始异常。假设程序中通过arg[0]获取用户输入的路径参数,如果路径不存在则抛出原始异常:FileNotFoundException。这种方式会泄露服务器的目录结构,攻击者可以通过尝试输入不同的文件路径来判断服务器对应的文件是否存在,从而还原出服务器的目录结构

  2. 用户请求目录不存在,封装一个自定义异常并抛出。攻击原理同上

  3. 用户请求目录不存在,抛出任意异常。攻击原理同上,因为仅仅是抛出异常这个动作都有可能被攻击者利用

防御办法:

  1. 校验用户输入的目录位置,该位置只能是限定在某个白名单中规定的目录位置
  2. 如果目录位置不存在,则只给出简单提示,如访问失败、操作失败等,不能给出太多信息,如文件/目录不存在、用户权限不够等

序列化敏感信息

如果敏感信息在序列化以后进行传输,那么在传输过程中可能会被泄露。出现的原因可能是:

  1. 序列化了不该序列化的敏感数据
  2. 未对敏感数据进行加密和签名

防御办法:

  1. 在Java中,如果某个类的字段不需要序列化,则可以使用关键字transient修饰
  2. 将敏感数据先加密再签名,然后才序列化和传输。注:如果传输过程中使用了SSL/TLS,则可以不加密和签名,因为传输通道本身就是安全的

线程同步

在Java中线程同步有若干问题需要考虑,下面逐一讨论。

防止锁对象被暴露到不可信区域

需要防止锁对象被暴露到不可信区域,因为如果被暴露,则攻击者可以访问到该锁对象,然后一直持有该对象锁,从而引发DOS攻击。攻击者一直持有对象锁的方式可能是:

  1. 攻击者获取到对象锁以后,使对象锁处于while (true){}循环中,不释放对象锁
  2. 攻击者获取到对象锁以后,使当前线程一直sleep,不释放对象锁

防止使用可被重用的对象来加锁

不能使用可被重用的对象来加锁,如:

  1. Boolean.False
  2. Boolean.True
  3. new String("...").intern()

因为这些对象在Java中只有一个全局实例,使用这些全局实例来充当锁对象,就相当于暴露了这些锁对象。

防止使用Object.getClass()方法来获取锁对象

因为在继承时,子类和父类的getClass()方法得到的类对象可能是不一样的,会导致不可预期的问题,如死锁等。

防止使用实例对象锁来控制静态变量

静态变量是全局的,所有实例对象共享该数据。使用实例对象锁,只能保证一个实例的不同方法间的互斥,不能保证不同实例间的方法互斥。控制静态变量需要使用静态对象锁。

防止使用高层并发锁对象进行同步与互斥

因为高层并发锁对象本身就提供了锁的功能,如ReentrantLocklock()方法,因此就没必要让ReentrantLock对象本身作为锁对象放置到synchronized代码块中。

防止子类使用非线程方法覆盖父类的线程安全方法

如果父类的线程安全方法被覆盖,则子类调用该方法时将不再线程安全。

防止出现死锁

死锁一定出现在线程间的循环等待中,一般有以下场景:

  1. 抛出异常时没有释放锁。需要使用try-catch-finally语句,并在fianlly中释放锁
  2. 两个或多个线程出现循环等待,即一个线程在持有某个锁的同时还试图获取其它的锁。要尽量避免让一个线程同时获取多个锁,如果一定要这样,需要认真分析是否有循环等待的可能

IO操作

本小节讨论IO操作需要考虑的问题。

临时文件使用后要及时删除

程序中创建的临时文件在使用后需要及时删除,如果不删除可能会泄露敏感信息。

不要将Buffer封装的数据暴露给不可信区域

因为Buffer的duplicateslicewap等方法都是浅拷贝,如果Buffer对象被其它地方访问到,则其包含的原始对象数据也可能被其它地方意外得到,从而导致原始敏感数据泄露或被篡改。

防御办法:

  1. 创建Buffer对象时使其只读,即调用方法asReadOnlyBuffer()
  2. 手动实现深拷贝

正确处理外部进程的输出流和错误流

当程序中调用一个外部命令时,如果外部命令对应的stdoutstderr缓冲区被写满,则外部程序由于无法继续往缓冲区写入数据,其会一直hang住,等待缓冲区数据被读取。如果缓冲区没有被及时读取(主程序一直等待外部命令返回),最终会导致主程序hang住。

防御办法:

  1. stdoutstderr分别创建线程实时读取和处理流数据

从流中读取一个字符或一个字节的方法的返回值需要使用int类型

对于从流中读取一个字符或一个字节的方法的返回值,需要使用int类型来接收该字符或字节,不能使用bytechar。因为InputStreamReader中方法的返回值就是int,使用bytechar可能导致数据被截断。


反序列化

序列化以后的数据通常会在网络上传输,传输过程中有可能被篡改或伪造,这将导致在反序列化时加载了非法的类到JVM或者反序列化了非法的对象。

避免直接使用ObjectInputStream进行反序列化

因为我们不能保证反序列化以后加载的类就是我们期望的类,有可能是被伪造的类。

防御办法:

  1. 创建一个自定义的继承自原始ObjectInputStream的类,并重写resolveClass()方法,对反序列化后的类名进行校验,检查是否在白名单中

使用第三方库反序列化JSON数据时需要关闭type功能

常用的第三方库有JacksonFastJson等,在使用这些库时需要关闭type功能,即调用方法setAutoTypeSupport(false),目的时不能把JSON转换成任意的对象,同时需要校验目标类名是否在白名单中。

使用第三方库反序列化XML数据时需要设置安全相关参数

可使用XStream等第三方库反序列化XMl数据,在反序列化时需要对目标类名进行白名单检查,并且设置安全相关参数,即调用方法setDefaultSecurity(...)


Java平台安全机制与组件

Java平台相关的组件有:

  1. ByteCodeVerifier
  2. ClassLoader
  3. SecurityManager
  4. AccessController
  5. Policy

下面讨论这些组件相关的安全问题。

安全检查方法被子类覆写

假设某个自定义的安全检查类SecurityChecker中定义了一个安全检查的方法checkSecurity(),该方法中包含如下检查的语句块:

...
SecurityManager sc = System.getSecurityManager();
sc.checkPermission(...)
...

如果某个SecurityChecker的子类覆盖了checkSecurity()方法,则该安全校验机制将可能失效。

防御办法:

  1. SecurityChecker中定义checkSecurity()方法为privatefinal
  2. 子类中显示调用父类的checkSecurity()方法,并增加子类的校验逻辑

自定义ClassLoader需要调用父类的getPermission()方法

自定义的ClassLoader需要调用父类的getPermission()方法以覆盖默认的getPermission()方法,否则自定义的ClassLoader在加载类时可能不会应用父类中定义的全局安全策略。

对特定操作可以施加权限校验

在对某个容器类对象(如MapList等)进行删除、修改、增加等操作时,可以进行权限校验,判断当前线程是否有相关操作的权限。代码片段:

...
SecurityManager sc = System.getSecurityManager();
sc.checkSecurityAccess(...)
...

避免使用自动签名机制检查jar包

避免使用URLClassLoaderjava.util.jar.XXX提供的自动签名检查机制检查jar包,应该在加载类的地方获取证书链,通过代码来校验。


其它安全问题

上面讨论的都是一些典型类型的安全问题,其实安全问题远不止这些,下面列举一些其它的安全问题。

避免使用伪随机数

程序中应该使用强随机数而不是伪随机数。

伪随机数使用示例:

Random random = new Random();

强随机数使用示例:

SecureRandom random = SecureRandom.getInstance("SHA1PRNG");

避免使用未加密的Socket

不要使用java.net.Socket类及其相关类,应该使用javax.net.ssl.SSLSocket。在使用javax.net.ssl.SSLSocket进行数据传输时,即便不进行证书校验,数据在传输过程中也是加密的,攻击者也无法解密在网络中传输的加密数据。

对口令等敏感数据进行Hash时必须加随机盐

盐值可以是固定长度的大于等于64位长度的随机字符串。将盐值+口令作为最终的口令,然后进行Hash,理论上攻击者不可能破解原口令。这样做得目的是防止彩虹表攻击,就算攻击者获取到了Hash值和盐值,其也很难在短时间内破解原口令等敏感数据。因为有两点原因可以保证:

  1. 彩虹表攻击本质上就是暴力破解,需要比较所有可能组合。如果彩虹表提前准备好,则破解速度可能较快,如果实时生成彩虹表,则破解速度非常慢。加上随机盐的彩虹表几乎不会提前存在,因为攻击者不可能提前知道盐值是什么
  2. 随机盐一般保存在服务器数据库中,攻击者一般不可能随意获取到

禁止将敏感数据硬编码在代码中

很多时候程序员喜欢在程序中硬编码敏感数据,如密码、token、key等,这个要绝对禁止,因为Java程序非常容易被反编译,一旦被反编译,这些敏感数据将会被泄露。

禁止使用String对象存放敏感数据

因为String对象会保存到JVM的常量池,一旦String对象被创建,该对象会一直存在直到JVM退出。如果JVM内存被dump到文件,则该文件会包含常量池中的敏感数据,在进行内存分析时敏感数据将会被泄露。

防御办法:

  1. 使用char[]数组保存敏感数据,在使用完以后清空char[]数组内容
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值