网络安全

位置:IT落伍者 >> 网络安全 >> 浏览文章

您的Java代码安全吗—还是暴露在外?


发布日期:2019年07月20日
 
您的Java代码安全吗—还是暴露在外?

摘要虽然客户仍然很关心您为他们构建的应用程序的可伸缩性和可用性但他们可能变得也很关心安全性而且要求特别严格应用程序可能容易受到两类安全性威胁的攻击静态和动态虽然开发人员不能完全控制动态威胁但在开发应用程序时您可以采取一些预防措施来消除静态威胁本文概括并解释了 种类型的静态暴露 ― 它们是系统中的缺陷它使系统暴露在想要篡夺该系统的特权的攻击者面前您将学会如何处理这些暴露以及如何发现(如果不处理这些暴露)这些暴露可能造成的影响

在开发Java Web应用程序时您需要确保应用程序拥有完善的安全性特征补充这里在谈到Java安全性时我们并不谈及Java 语言提供的安全性 API也不涉及使用Java代码来保护应用程序本文将着重讨论可能潜伏在您的 Java 应用程序中的 安全性暴露安全性暴露是系统中的缺陷它使系统无法 ― 即使系统被正常使用 ― 防止攻击者篡夺对系统的特权控制系统的运行危及系统上的数据安全或者假冒未经授权的信任相对于安全性暴露许多开发人员更加关心网站的感官效果

毫无疑问客户现在既严格地关注性能可伸缩性和可用性也严格地关注安全性应用程序可能容易受到两类安全性威胁的攻击 动态和 静态动态威胁是那些同未经授权进入系统有关的威胁或那些同跨越网络传输的数据的完整性隐私和机密性有关的威胁这些威胁同应用程序的功能代码没有多大关系使用加密加密术和认证技术来消除这些威胁相比之下静态威胁却同应用程序的功能代码 有关它们同进入系统的授权用户所做的事情有关未知用户闯入系统是动态威胁的一个示例授权用户以未授权方式操作系统内的代码或数据是静态威胁的示例应用程序开发人员并不能完全控制动态威胁但开发人员在构建应用程序时却可以采取预防措施来消除静态威胁

在本文中我们讨论了对付 种不同静态暴露的技巧对于每种暴露我们解释了不处理这些安全性问题所造成的影响我们还为您推荐了一些准则要开发不受这些静态安全性暴露威胁的健壮且安全的 Java 应用程序您应该遵循这些准则一有合适的时机我们就提供代码样本(既有暴露的代码也有无暴露的代码)

对付高严重性暴露的技巧

请遵循下列建议以避免高严重性静态安全性暴露

限制对变量的访问

让每个类和方法都成为 final除非有足够的理由不这样做

不要依赖包作用域

使类不可克隆

使类不可序列化

使类不可逆序列化

避免硬编码敏感数据

查找恶意代码

限制对变量的访问

如果将变量声明为 public那么外部代码就可以操作该变量这可能会导致安全性暴露

影响

如果实例变量为 public 那么就可以在类实例上直接访问和操作该实例变量将实例变量声明为 protected 并不一定能解决这一问题虽然不可能直接在类实例基础上访问这样的变量但仍然可以从派生类访问这个变量

清单 演示了带有 public 变量的代码因为变量为 public 的所以它暴露了

清单 带有 public 变量的代码

class Test {

public int id;

protected String name;

Test(){

id = ;

name = hello world;

}

//code

}

public class MyClass extends Test{

public void methodIllegalSet(String name){

thisname = name; // this should not be allowed

}

public static void main(String[] args){

Test obj = new Test();

objid = ; // this should not be allowed

MyClass mc = new MyClass();

thodIllegalSet(Illegal Set Value);

}

}

建议

一般来说应该使用取值方法而不是 public 变量按照具体问题具体对待的原则在确定哪些变量特别重要因而应该声明为 private 时请将编码的方便程度及成本同安全性需要加以比较清单 演示了以下列方式来使之安全的代码

清单 不带有 public 变量的代码

class Test {

private int id;

private String name;

Test(){

id = ;

name = hello world;

}

public void setId(int id){

thisid = id;

}

public void setName(String name){

thisname = name;

}

public int getId(){

return id;

}

public String getName(){

return name;

}

}

让每个类和方法都为 final

不允许扩展的类和方法应该声明为 final 这样做防止了系统外的代码扩展类并修改类的行为

影响

仅仅将类声明为非 public 并不能防止攻击者扩展类因为仍然可以从它自己的包内访问该类

建议

让每个类和方法都成为 final除非有足够的理由不这样做按此建议我们要求您放弃可扩展性虽然它是使用诸如 Java 语言之类的面向对象语言的主要优点之一在试图提供安全性时可扩展性却成了您的敌人可扩展性只会为攻击者提供更多给您带来麻烦的方法

不要依赖包作用域

没有显式地标注为 public private 或 protected 的类方法和变量在它们自己的包内是可访问的

影响

如果 Java 包不是封闭的那么攻击者就可以向包内引入新类并使用该新类来访问您想保护的内容诸如 javalang 之类的一些包缺省是封闭的一些 JVM 也让您封闭自己的包然而您最好假定包是不封闭的

建议

从软件工程观点来看包作用域具有重要意义因为它可以阻止对您想隐藏的内容进行偶然的无意中的访问但不要依靠它来获取安全性应该将类方法和变量显式标注为 public private 或 protected 中适合您特定需求的那种

使类不可克隆

克隆允许绕过构造器而轻易地复制类实例

影响

即使您没有有意使类可克隆外部源仍然可以定义您的类的子类并使该子类实现 javalangCloneable 这就让攻击者创建了您的类的新实例拷贝现有对象的内存映象生成了新的实例虽然这样做有时候是生成新对象的可接受方法但是大多数时候是不可接受的清单 说明了因为可克隆而暴露的代码

清单 可克隆代码

class MyClass{

private int id;

private String name;

public MyClass(){

id=;

name=HaryPorter;

}

public MyClass(int idString name){

thisid=id;

thisname=name;

}

public void display(){

Systemoutprintln(Id =+id+\n+Name=+name);

}

}

// hackers code to clone the user class

public class Hacker extends MyClass implements Cloneable {

public static void main(String[] args){

Hacker hack=new Hacker();

try{

MyClass o=(MyClass)hackclone();

odisplay();

}

catch(CloneNotSupportedException e){

eprintStackTrace();

}

}

}

建议

要防止类被克隆可以将清单 中所示的方法添加到您的类中

清单 使您的代码不可克隆

public final Object clone()

throws javalangCloneNotSupportedException{

throw new javalangCloneNotSupportedException();

}

如果想让您的类可克隆并且您已经考虑了这一选择的后果那么您仍然可以保护您的类要做到这一点请在您的类中定义一个为 final 的克隆方法并让它依赖于您的一个超类中的一个非 final 克隆方法如清单 中所示

清单 以安全的方式使您的代码可克隆

public final Object clone()

throws javalangCloneNotSupportedException {

superclone();

}

类中出现 clone() 方法防止攻击者重新定义您的 clone 方法

使类不可序列化

序列化允许将类实例中的数据保存在外部文件中闯入代码可以克隆或复制实例然后对它进行序列化

影响

序列化是令人担忧的因为它允许外部源获取对您的对象的内部状态的控制这一外部源可以将您的对象之一序列化成攻击者随后可以读取的字节数组这使得攻击者可以完全审查您的对象的内部状态包括您标记为 private 的任何字段它也允许攻击者访问您引用的任何对象的内部状态

建议

要防止类中的对象被序列化请在类中定义清单 中的 writeObject() 方法

清单 防止对象序列化

private final void writeObject(ObjectOutputStream out)

throws javaioNotSerializableException {

throw new javaioNotSerializableException(This object cannot

be serialized);

}

通过将 writeObject() 方法声明为 final防止了攻击者覆盖该方法

使类不可逆序列化

通过使用逆序列化攻击者可以用外部数据或字节流来实例化类

影响

不管类是否可以序列化都可以对它进行逆序列化外部源可以创建逆序列化成类实例的字节序列这种可能为您带来了大量风险因为您不能控制逆序列化对象的状态请将逆序列化作为您的对象的另一种公共构造器 ― 一种您无法控制的构造器

建议

要防止对对象的逆序列化应该在您的类中定义清单 中的 readObject() 方法

清单 防止对象逆序列化

private final void readObject(ObjectInputStream in)

throws javaioNotSerializableException {

throw new javaioNotSerializableException(This object cannot

be deserialized);

}

通过将该方法声明为 final 防止了攻击者覆盖该方法

避免硬编码敏感数据

您可能会尝试将诸如加密密钥之类的秘密存放在您的应用程序或库的代码对于你们开发人员来说这样做通常会把事情变得更简单

影响

任何运行您的代码的人都可以完全访问以这种方法存储的秘密没有什么东西可以防止心怀叵测的程序员或虚拟机窥探您的代码并了解其秘密

建议

可以以一种只可被您解密的方式将秘密存储在您代码中在这种情形下秘密只在于您的代码所使用的算法这样做没有多大坏处但不要洋洋得意认为这样做提供了牢固的保护您可以 遮掩您的源代码或字节码 ― 也就是以一种为了解密必须知道加密格式的方法对源代码或字节码进行加密 ― 但攻击者极有可能能够推断出加密格式对遮掩的代码进行逆向工程从而揭露其秘密

这一问题的一种可能解决方案是将敏感数据保存在属性文件中无论什么时候需要这些数据都可以从该文件读取如果数据极其敏感那么在访问属性文件时您的应用程序应该使用一些加密/解密技术

查找恶意代码

从事某个项目的某个心怀叵测的开发人员可能故意引入易受攻击的代码打算日后利用它这样的代码在初始化时可能会启动一个后台进程该进程可以为闯入者开后门它也可以更改一些敏感数据

这样的恶意代码有三类

类中的 main 方法

定义过且未使用的方法

注释中的死代码

影响

入口点程序可能很危险而且有恶意通常Java 开发人员往往在其类中编写 main() 方法这有助于测试单个类的功能当类从测试转移到生产环境时带有 main() 方法的类就成为了对应用程序的潜在威胁因为闯入者将它们用作入口点

请检查代码中是否有未使用的方法出现这些方法在测试期间将会通过所有的安全检查因为在代码中不调用它们 ― 但它们可能含有硬编码在它们内部的敏感数据(虽然是测试数据)引入一小段代码的攻击者随后可能调用这样的方法

避免最终应用程序中的死代码(注释内的代码)如果闯入者去掉了对这样的代码的注释那么代码可能会影响系统的功能性

可以在清单 中看到所有三种类型的恶意代码的示例

清单 潜在恶意的 Java 代码

public void unusedMethod(){

// code written to harm the system

}

public void usedMethod(){

//unusedMethod(); //code in comment put with bad intentions

//might affect the system if uncommented

// int x = ;

// x=x+; //Code in comment might affect the

//functionality of the system if uncommented

}

建议

应该将(除启动应用程序的 main() 方法之外的) main() 方法未使用的方法以及死代码从应用程序代码中除去在软件交付使用之前主要开发人员应该对敏感应用程序进行一次全面的代码评审应该使用Stubdummy类代替 main() 方法以测试应用程序的功能

对付中等严重性暴露的技巧

请遵循下列建议以避免中等严重性静态安全性暴露

不要依赖初始化

不要通过名称来比较类

不要使用内部类

不要依赖初始化

您可以不运行构造器而分配对象这些对象使用起来不安全因为它们不是通过构造器初始化的

影响

在初始化时验证对象确保了数据的完整性

例如请想象为客户创建新帐户的 Account 对象只有在 Account 期初余额大于 才可以开设新帐户可以在构造器里执行这样的验证有些人未执行构造器而创建 Account 对象他可能创建了一个具有一些负值的新帐户这样会使系统不一致容易受到进一步的干预

建议

在使用对象之前请检查对象的初始化过程要做到这一点每个类都应该有一个在构造器中设置的私有布尔标志如清单 中的类所示在每个非 static 方法中代码在任何进一步执行之前都应该检查该标志的值如果该标志的值为 true 那么控制应该进一步继续否则控制应该抛出一个例外并停止执行那些从构造器调用的方法将不会检查初始化的变量因为在调用方法时没有设置标志因为这些方法并不检查标志所以应该将它们声明为 private 以防止用户直接访问它们

清单 使用布尔标志以检查初始化过程

public class MyClass{

private boolean initialized = false;

//Other variables

public MyClass (){

//variable initialization

method();

initialized = true;

}

private void method(){ //no need to check for initialization variable

//code

}

public void method(){

try{

if(initialized==true){

//proceed with the business logic

}

else{

throw new Exception(Illegal State Of the object);

}

}catch(Exception e){

eprintStackTrace();

}

}

}

如果对象由逆序列化进行初始化那么上面讨论的验证机制将难以奏效因为在该过程中并不调用构造器在这种情况下类应该实现 ObjectInputValidation 接口

清单 实现 ObjectInputValidation

interface javaioObjectInputValidation {

public void validateObject() throws InvalidObjectException;

}

所有验证都应该在 validateObject() 方法中执行对象还必须调用 ObjectInputStreamRegisterValidation() 方法以为逆序列化对象之后的验证进行注册 RegisterValidation() 的第一个参数是实现 validateObject() 的对象通常是对对象自身的引用任何实现 validateObject() 的对象都可能充当对象验证器但对象通常验证它自己对其它对象的引用 RegisterValidation() 的第二个参数是一个确定回调顺序的整数优先级优先级数字大的比优先级数字小的先回调同一优先级内的回调顺序则不确定

当对象已逆序列化时 ObjectInputStream 按照从高到低的优先级顺序调用每个已注册对象上的 validateObject()

不要通过名称来比较类

有时候您可能需要比较两个对象的类以确定它们是否相同或者您可能想看看某个对象是否是某个特定类的实例因为 JVM 可能包括多个具有相同名称的类(具有相同名称但却在不同包内的类)所以您不应该根据名称来比较类

影响

如果根据名称来比较类您可能无意中将您不希望授予别人的权利授予了闯入者的类因为闯入者可以定义与您的类同名的类

例如请假设您想确定某个对象是否是类 combarFoo 的实例清单 演示了完成这一任务的错误方法

清单 比较类的错误方法

if(objgetClass()getName()equals(Foo)) // Wrong!

// objects class is named Foo

}else{

// objects class has some other name

}

建议

在那些非得根据名称来比较类的情况下您必须格外小心必须确保使用了当前类的 ClassLoader 的当前名称空间如清单 中所示

清单 比较类的更好方法

if(objgetClass() == thisgetClassLoader()loadClass(combarFoo)){

// objects class is equal to

//the class that this class calls combarFoo

}else{

// objects class is not equal to the class that

// this class calls combarFoo

}

然而比较类的更好方法是直接比较类对象看它们是否相等例如如果您想确定两个对象 a 和 b 是否属同一个类那么您就应该使用清单 中的代码

清单 直接比较对象来看它们是否相等

if(agetClass() == bgetClass()){

// objects have the same class

}else{

// objects have different classes

}

尽可能少用直接名称比较

不要使用内部类

Java 字节码没有内部类的概念因为编译器将内部类转换成了普通类而如果没有将内部类声明为 private 则同一个包内的任何代码恰好能访问该普通类

影响

因为有这一特性所以包内的恶意代码可以访问这些内部类如果内部类能够访问括起外部类的字段那么情况会变得更糟可能已经将这些字段声明为 private 这样内部类就被转换成了独立类但当内部类访问外部类的字段时编译器就将这些字段从专用(private)的变为在包(package)的作用域内有效的内部类暴露了已经够糟糕的了但更糟糕的是编译器使您将某些字段成为 private 的举动成为徒劳

建议

如果能够不使用内部类就不要使用内部类

对付低严重性暴露的技巧

请遵循下列建议以避免低严重性静态安全性暴露

避免返回可变对象

检查本机方法

避免返回可变对象

Java 方法返回对象引用的副本如果实际对象是可改变的那么使用这样一个引用调用程序可能会改变它的内容通常这是我们所不希望见到的

影响

请考虑这个示例某个方法返回一个对敏感对象的内部数组的引用假定该方法的调用程序不改变这些对象即使数组对象本身是不可改变的也可以在数组对象以外操作数组的 内容这种操作将反映在返回该数组的对象中如果该方法返回可改变的对象那么事情会变得更糟外部实体可以改变在那个类中声明的 public 变量这种改变将反映在实际对象中

清单 演示了脆弱性 getExposedObj() 方法返回了 Exposed 对象的 引用副本该对象是可变的

清单 返回可变对象的引用副本

class Exposed{

private int id;

private String name;

public Exposed(){

}

public Exposed(int id String name){

thisid = id;

thisname = name;

}

public int getId(){

return id;

}

public String getName(){

return name;

}

public void setId(int id){

thisid=id;

}

public void setName(String name){

thisname = name;

}

public void display(){

Systemoutprintln(Id = + id + Name = + name);

}

}

public class Exp{

private Exposed exposedObj = new Exposed(Harry Porter);

public Exposed getExposedObj(){

return exposedObj; //returns a reference to the object

}

public static void main(String[] args){

Exp exp = new Exp();

expgetExposedObj()display();

Exposed exposed = expgetExposedObj();

exposedsetId();

exposedsetName(Hacker);

expgetExposedObj()display();

}

}

建议

如果方法返回可改变的对象但又不希望调用程序改变该对象请修改该方法使之不返回实际对象而是返回它的副本或克隆要改正清单 中的代码请让它返回 Exposed 对象的 副本如清单 中所示

清单 返回可变对象的副本

public Exposed getExposedObj(){

return new Exposed(exposedObjgetId()exposedObjgetName());

}

或者您的代码也可以返回 Exposed 对象的克隆

检查本机方法

本机方法是一种 Java 方法其实现是用另一种编程语言编写的如 C 或 C++有些开发人员实现本机方法这是因为 Java 语言即使使用即时(justintime)编译器也比许多编译过的语言要慢其它人需要使用本机代码是为了在 JVM 以外实现特定于平台的功能

影响

使用本机代码时请小心因为对这些代码进行验证是不可能的而且本机代码可能潜在地允许 applet 绕过通常的安全性管理器(Security Manager)和 Java 对设备访问的控制

建议

如果非得使用本机方法那么请检查这些方法以确定

它们返回什么

它们获取什么作为参数

它们是否绕过安全性检查

它们是否是 public private 等等

它们是否含有绕过包边界从而绕过包保护的方法调用

结束语

编写安全 Java 代码是十分困难的但本文描述了一些可行的实践来帮您编写安全 Java 代码这些建议并不能解决您的所有安全性问题但它们将减少暴露数目最佳软件安全性实践可以帮助确保软件正常运行安全至关重要和高可靠系统设计者总是花费大量精力来分析和跟蹤软件行为只有通过将安全性作为至关紧要的系统特性来对待 ― 并且从一开始就将它构建到应用程序中我们才可以避免亡羊补牢似的修修补补的安全性方法

               

上一篇:java的线程安全四种方式五个等级

下一篇:安全连接方式SSL