《Java编码指南:编写安全可靠程序的75条建议》—— 指南15:不要依赖可以被不可信代码覆盖的方法...

    xiaoxiao2024-05-15  142

    本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.15节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。

    指南15:不要依赖可以被不可信代码覆盖的方法

    不可信代码可以滥用可信代码提供的API来覆盖一些方法,如Object.equals()、Object. hashCode()和Thread.run()。这些方法是很重要的目标,因为它们通常是在幕后被使用,很可能以不容易辨别的方式与其他组件进行交互。

    通过提供覆盖的实现,攻击者可以使用不可信的代码来收集敏感信息、运行任意代码,或者发起拒绝服务攻击。

    关于覆盖Object.clone()方法的更多详细信息参见指南10。

    违规代码示例(hashCode)

    下面的违规代码示例展示了一个LicenseManager类,它维持着一个licenseMap。这个映射存储的是许可证类型(LicenseType)和许可证值对。

    public class LicenseManager {  Map<LicenseType, String> licenseMap =   new HashMap<LicenseType, String>();  public LicenseManager() {   LicenseType type = new LicenseType();   type.setType("demo-license-key");   licenseMap.put(type, "ABC-DEF-PQR-XYZ");  }  public Object getLicenseKey(LicenseType licenseType) {   return licenseMap.get(licenseType);  }  public void setLicenseKey(LicenseType licenseType,               String licenseKey) {   licenseMap.put(licenseType, licenseKey);  } } class LicenseType {  private String type;  public String getType() {   return type;  }  public void setType(String type) {   this.type = type;  }  @Override  public int hashCode() {   int res = 17;   res = res * 31 + type == null ? 0 : type.hashCode();   return res;  }  @Override  public boolean equals(Object arg) {   if (arg == null || !(arg instanceof LicenseType)) {    return false;   }   if (type.equals(((LicenseType) arg).getType())) {    return true;   }   return false;  } }``` LicenseManager的构造函数用必须保持密文的演示许可证密钥,对licenseMap进行了初始化。为便于说明,许可证密钥是硬编码的;理想情况下应该从外部配置文件中读取经加密后存储的密钥。LicenseType类提供了equals()方法和hashCode()方法的覆盖实现。 这个实现是易受攻击的,攻击者可以扩展LicenseType类并覆盖equals()方法和hashCode()方法:

    public class CraftedLicenseType extends LicenseType { private static int guessedHashCode = 0; @Override public int hashCode() {  // Returns a new hashCode to test every time get() is called  guessedHashCode++;  return guessedHashCode; } @Override public boolean equals(Object arg) {  // Always returns true  return true; }}`下面是恶意的客户端程序:

    public class DemoClient {  public static void main(String[] args) {   LicenseManager licenseManager = new LicenseManager();   for (int i = 0; i <= Integer.MAX_VALUE; i++) {    Object guessed =     licenseManager.getLicenseKey(new CraftedLicenseType());    if (guessed != null) {     // prints ABC-DEF-PQR-XYZ     System.out.println(guessed);    }   }  } }``` 客户端程序使用CraftedLicenseType类遍历所有可能的散列码序列,直到它成功匹配到存储在LicenseManager类中的演示许可证密钥对象的散列码。因此,仅仅只需几分钟,攻击者就可以发现licenseMap中的敏感数据。这个攻击是通过发现至少一个关于映射中键的散列冲突进行的。 ####合规解决方案(IdentityHashMap) 下面的合规解决方案使用了一个IdentityHashMap来存储许可证信息,而不是HashMap。

    public class LicenseManager { Map licenseMap =  new IdentityHashMap();

     // ...}`根据Java API中IdentityHashMap类的文档[API 2006]:

    这个类以一个散列表实现Map(映射)接口,在比较键(和值)时使用引用相等代替对象相等。换句话说,如果在一个IdentityHashMap中有k1和k2两个键,那么当且仅当(k1==k2)时,才可以说它们是相等的。(而对于普通的Map实现(如HashMap)中的两个键k1和k2,当且仅当(k1==null ? k2==null : k1.equals(k2))时,才可以说它们是相等的。)

    因此,覆盖方法不能暴露内部类的细节。客户端程序可以继续添加许可证密钥,甚至可以检索添加的键值对,如下列客户端代码所示。

    public class DemoClient {  public static void main(String[] args) {   LicenseManager licenseManager = new LicenseManager();   LicenseType type = new LicenseType();   type.setType("custom-license-key");   licenseManager.setLicenseKey(type, "CUS-TOM-LIC-KEY");   Object licenseKeyValue = licenseManager.getLicenseKey(type);   // Prints CUS-TOM-LIC-KEY   System.out.println(licenseKeyValue);  } }``` ####合规解决方案(final类) 下面的合规解决方案将LicenseType类用final关键字声明成了不可更改的类,这样它的所有方法就都不能被覆盖了。

    final class LicenseType { // ...}`

    违规代码示例

    下面的违规代码示例包含一个Widget类和一个含有一组部件的LayoutManager类。

    public class Widget {  private int noOfComponents;  public Widget(int noOfComponents) {   this.noOfComponents = noOfComponents;  }  public int getNoOfComponents() {   return noOfComponents;  }  public final void setNoOfComponents(int noOfComponents) {   this.noOfComponents = noOfComponents;  }  public boolean equals(Object o) {   if (o == null || !(o instanceof Widget)) {    return false;   }   Widget widget = (Widget) o;   return this.noOfComponents == widget.getNoOfComponents();  }  @Override  public int hashCode() {   int res = 31;   res = res * 17 + noOfComponents;   return res;  } } public class LayoutManager {  private Set<Widget> layouts = new HashSet<Widget>();  public void addWidget(Widget widget) {   if (!layouts.contains(widget)) {    layouts.add(widget);   }  }  public int getLayoutSize() {   return layouts.size();  } }``` 攻击者可以用Navigator部件扩展Widget类,并覆盖hashCode()方法:

    public class Navigator extends Widget { public Navigator(int noOfComponents) {  super(noOfComponents); } @Override public int hashCode() {  int res = 31;  res = res * 17;  return res; }}`客户端代码如下:

    Widget nav = new Navigator(1); Widget widget = new Widget(1); LayoutManager manager = new LayoutManager(); manager.addWidget(nav); manager.addWidget(widget); System.out.println(manager.getLayoutSize()); // Prints 2``` layouts(布局)集合本应只包含一个条目,因为被添加的Navigator和Widget的组件数量都是1。然而,getLayoutSize()方法确返回了2。 产生这种差异的原因是,Widget的hashCode()方法只在Widget对象被添加到集合中时使用了一次。当添加Navigator时,集合使用的是Navigator类提供的hashCode()方法。因此,集合中包含两个不同的对象实例。 ####合规解决方案(final类) 下面的合规解决方案将Widget类声明成final类,这样它的方法就不能被覆盖了。

    public final class Widget { // ...}`

    违规代码示例(run())

    在下面的违规代码示例中,Worker类及其子类SubWorker,均包含一个用来启动一个线程的startThread()方法。

    public class Worker implements Runnable {  Worker() { }  public void startThread(String name) {   new Thread(this, name).start();  }  @Override  public void run() {   System.out.println("Parent");  } } public class SubWorker extends Worker {  @Override  public void startThread(String name) {   super.startThread(name);   new Thread(this, name).start();  }  @Override  public void run() {   System.out.println("Child");  } }``` 如果一个客户端运行下面的代码:

    Worker w = new SubWorker();w.startThread("thread");`客户端可能会希望Parent和Child都被打印出来。然而,Child会被打印两次,因为被覆盖的方法run()在启动一个新线程时被调用了两次。

    合规解决方案

    下面的合规解决方案修改了SubWorkder类,移除了对super.startThread()的调用。

    public class SubWorker extends Worker {  @Override  public void startThread(String name) {   new Thread(this, name).start();  }  // ... }``` 对客户端代码也做了修改,单独开启父线程和子线程。这个程序将会产生预期的输出:

    Worker w1 = new Worker();w1.startThread("parent-thread");Worker w2 = new SubWorker();w2.startThread("child-thread");

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)