第一,运行时安全机制。可以简单认为,就是限制 Java 运行时的行为,不要做越权或者不靠谱的事情,具体来看:
-
在类加载过程中,进行字节码验证,以防止不合规的代码影响 JVM 运行或者载入其他恶意代码。
-
类加载器本身也可以对代码之间进行隔离,例如,应用无法获取启动类加载器(Bootstrap Class-Loader)对象实例,不同的类加载器也可以起到容器的作用,隔离模块之间不必要的可见性等。目前,Java Applet、RMI 等特性已经或逐渐退出历史舞台,类加载等机制总体上反倒在不断简化。
-
利用 SecurityManger 机制和相关的组件,限制代码的运行时行为能力,其中,你可以定制 policy 文件和各种粒度的权限定义,限制代码的作用域和权限,例如对文件系统的操作权限,或者监听某个网络端口的权限等。我画了一个简单的示意图,对运行时安全的不同层次进行了整理。
可以看到,Java 的安全模型是以代码为中心的,贯穿了从类加载,如 URLClassLoader 加载网络上的 Java 类等,到应用程序运行时权限检查等全过程。
- 另外,从原则上来说,Java 的 GC 等资源回收管理机制,都可以看作是运行时安全的一部分,如果相应机制失效,就会导致 JVM 出现 OOM 等错误,可看作是另类的拒绝服务。
第二,Java 提供的安全框架 API,这是构建安全通信等应用的基础。例如:
注意,这一部分 API 内部实现是和厂商相关的,不同 JDK 厂商往往会定制自己的加密算法实现。
第三, 就是 JDK 集成的各种安全工具,例如:
在应用实践中,如果对安全要求非常高,建议打开 SecurityManager,
|
请注意其开销,通常只要开启 SecurityManager,就会导致 10% ~ 15% 的性能下降,在 JDK 9 以后,这个开销有所改善。
理解了基础 Java 安全机制,接下来我们来一起探讨安全漏洞(Vulnerability)。
按照传统的定义,任何可以用来绕过系统安全策略限制的程序瑕疵,都可以算作安全漏洞。具体原因可能非常多,设计或实现中的疏漏、配置错误等,任何不慎都有可能导致安全漏洞出现,例如恶意代码绕过了 Java 沙箱的限制,获取了特权等。如果你想了解更多安全漏洞的信息,可以从通用安全漏洞库(CVE)等途径获取,了解安全漏洞评价标准。
但是,要达到攻击的目的,未必都需要绕过权限限制。比如利用哈希碰撞发起拒绝服务攻击(DOS,Denial-Of-Service attack),常见的场景是,攻击者可以事先构造大量相同哈希值的数据,然后以 JSON 数据的形式发送给服务器端,服务器端在将其构建成为 Java 对象过程中,通常以 Hastable 或 HashMap 等形式存储,哈希碰撞将导致哈希表发生严重退化,算法复杂度可能上升一个数量级(HashMap 后续进行了改进,我在专栏第 9 讲介绍了树化机制),进而耗费大量 CPU 资源。
像这种攻击方式,无关于权限,可以看作是程序实现的瑕疵,给了攻击者以低成本进行进攻的机会。
我在开头提到的各种注入式攻击,可以有不同角度、不同层面的解决方法,例如针对 SQL 注入:
-
在数据输入阶段,填补期望输入和可能输入之间的鸿沟。可以进行输入校验,限定什么类型的输入是合法的,例如,不允许输入标点符号等特殊字符,或者特定结构的输入。
-
在 Java 应用进行数据库访问时,如果不用完全动态的 SQL,而是利用 PreparedStatement,可以有效防范 SQL 注入。不管是 SQL 注入,还是 OS 命令注入,程序利用字符串拼接生成运行逻辑都是个可能的风险点!
-
在数据库层面,如果对查询、修改等权限进行了合理限制,就可以在一定程度上避免被注入删除等高破坏性的代码。
在安全领域,有一句准则:安全倾向于 “明显没有漏洞”,而不是“没有明显漏洞”。所以,为了更加安全可靠的服务,我们最好是采取整体性的安全设计和综合性的防范手段,而不是头痛医头、脚痛医脚的修修补补,更不能心存侥幸。
一个比较普适的建议是,尽量使用较新版本的 JDK,并使用推荐的安全机制和标准。如果你有看过 JDK release notes,例如8u141,你会发现 JDK 更新会修复已知的安全漏洞,并且会对安全机制等进行增强。但现实情况是,相当一部分应用还在使用很古老的不安全版本 JDK 进行开发,并且很多信息处理的也很随意,或者通过明文传输、存储,这些都存在暴露安全隐患的可能。