Java的内部类是否会带来安全风险?

32

最近,我的项目安全团队发布了一份安全代码指南文件,旨在作为我们的代码审查的一部分使用。让我印象深刻的第一件事是其中一项规定:“不要使用内部类”。我认为这似乎是一个非常严厉和笼统的声明。如果使用得当,内部类是有用的,但我做了一些谷歌搜索,并发现了这个链接,为了方便引用。

规则5:不要使用内部类

一些Java语言的书籍说,内部类只能被包含它们的外部类访问。这是不正确的。Java字节码没有内部类的概念,因此编译器将内部类转换为普通类,可以被同一包中的任何代码访问。而规则4建议不要依赖包范围进行保护。

但是等等,情况变得更糟了。一个内部类可以访问封闭外部类的字段,即使这些字段声明为私有。而内部类被翻译成一个单独的类。为了允许这个单独的类访问外部类的字段,编译器会将这些字段从私有更改为包范围!内部类已经暴露出来很糟糕了,更糟糕的是编译器正在默默地推翻你做某些字段私有的决定。如果可以避免,请不要使用内部类。(具有讽刺意味的是,新的Java 2 doPrivileged() API使用指南建议您使用内部类编写特权代码。这是我们不喜欢doPrivileged() API的原因之一。)

我的问题是:

  1. 这种行为在Java 5/6中是否仍然存在?
  2. 考虑到除了外部和内部类之外的任何类都无法访问外部类的私有成员变量,这实际上是否构成安全风险?
  3. 它是否构成足够的安全风险,值得采用“不使用内部类”这一指导方针?

2
等一下?这是不是意味着“我不能使用Swing中最流行的创建回调的方式?” - Artyom
9
如果这确实是贵公司在代码审查期间的安全策略,请立即将贵公司报告给dailywtf.com。 - Zac Bowling
2
@Zac Bowling:thedailywtf.com是一个真正的网站,而另一个只是一个门户页面。 - nalply
1
Zac Bowling:特别是JDK充满了内部类(正如引用中所提到的)。 - Tom Hawtin - tackline
11
代码的可见性从来不是安全特性。即使是私有成员也可以使用反射访问。 - skaffman
显示剩余3条评论
10个回答

19

这些信息已经过时约十年。广泛使用带有AccessController.doPrivileged的匿名内部类应该是一个提示。(如果您不喜欢API,请考虑在JDK中错误地缺少try-finally块的比例。)

规则是,如果由不同的类加载器加载或具有不同的证书,则不能共享相同包的两个类。为了更好的保护,请在您的jar文件清单中将包标记为密封。因此,从安全角度来看,“第4条规则”是错误的,因此这个规则也是错误的。

无论如何,制定安全策略都应该理解您正在保护什么。这些类型的策略用于处理可能具有不同信任级别的移动代码(移动的代码)。除非您正在处理移动代码,或者您的代码将进入可能需要的库,否则这些预防措施几乎没有任何意义。但是,始终使用强大的编程风格几乎总是一个好主意,例如复制和验证参数和返回值。


复制参数并不能总是有助于使您的代码更安全,例如当您在参数收集上调用toArray()时,您就信任了参数的toArray()实现! - akuhn
Adrian:是的,正如 Sun 公司为 Java 所制定的安全编码指南中所指明的那样。 - Tom Hawtin - tackline

10
这种行为在Java 5/6中是否仍然存在呢?
并不完全如描述的那样。我从未见过这种情况的编译器:
为了让这个独立类可以访问外部类的字段,编译器会悄悄地将这些字段从private更改为package范围!
相反,据我所知Sun Java 3/4创建了一个访问器而不是修改字段。
Sun Java 6 (javac 1.6.0_16) 创建了一个静态访问器:
public class InnerExample {
    private int field = 42; 

    private class InnerClass {
        public int getField () { return field; };
    }

    private InnerClass getInner () { 
        return new InnerClass();
    }

    public static void main (String...args) {
        System.out.println(new InnerExample().getInner().getField());
    }
}


$ javap -classpath bin -private InnerExample
Compiled from "InnerExample.java"
public class InnerExample extends java.lang.Object{
    private int field;
    public InnerExample();
    private InnerExample$InnerClass getInner();
    public static void main(java.lang.String[]);
    static int access$000(InnerExample);
}


$ javap -classpath bin -c -private InnerExample
static int access$000(InnerExample);
  Code:
   0:   aload_0
   1:   getfield    #1; //Field field:I
   4:   ireturn

考虑到除了外部类和内部类以外的任何类都无法访问外部类的私有成员,那么这真的是一个安全隐患吗?

我这里有点猜测,但如果您编译该类,则不会出现问题,但是如果您添加了access$000,那么可以编译使用访问器的代码。

import java.lang.reflect.*;

public class InnerThief {
    public static void main (String...args) throws Exception {
        for (Method me : InnerExample.class.getDeclaredMethods()){
            System.out.println(me);
            System.out.printf("%08x\n",me.getModifiers());
        }

        System.out.println(InnerExample.access$000(new InnerExample()));
    }
}
有趣的是,合成的访问器具有修饰符标志00001008,而如果添加包级静态方法,则具有标志00000008。在JVM规范的第二版中没有关于该标志值的任何内容,但似乎可以防止javac看到该方法。
因此,似乎存在某些安全功能,但我找不到任何文档来说明这一点。
(因此发布此帖,以防万一有人知道类文件中0x1000的含义)

合成修饰符怎么样? - Serge Bogatyrev
我认为是这样的,但是没有在Sun的任何参考资料中找到相关信息,也没有任何东西表明“如果一个方法有合成修饰符,则无法通过反射调用它,除非首先将其设置为可访问”,这将消除所认为的安全风险。 - Pete Kirkham
1
寻找Java 6 JVMS时被重定向到过时的第二版文档是很奇怪的事情,甚至缺少明显的Java 5功能,如枚举、注解、泛型等。没有直接链接到第三版。Java 7版记录了ACC_SYNTHETIC修饰符。在Java 5之前,Synthetic属性用于相同的目的。 - Holger

9
  1. 是的,这种行为仍然存在。
  2. 这是一种安全风险,因为恶意类可以使用非标准javac进行构造。
  3. 这取决于你有多么偏执。如果您不允许外来类在JVM中运行,我认为没有问题。如果您允许外来类运行,则会有更大的问题(沙盒等)。
  4. 我知道您只有3个问题,但像其他人一样,我认为这是一个愚蠢的限制。

7
我会强调其中的第三点。如果攻击者已经侵入您的计算机/虚拟机,那么已经输了,无论进行多少次“安全编码”也无济于事。 - Esko
1
Esko:如果你运行了一个未经签名的小程序,你会输吗?我觉得不会! - Tom Hawtin - tackline
2
除非您已准备好适当的安全模型来处理它,并且您控制外部代码启动的方式,否则请勿使用@Esko。 - Jerome

6

这种行为在Java 5/6中是否仍然存在?

您可以使用javap工具来确定您的二进制文件正在暴露哪些内容以及如何处理。

package demo;
public class SyntheticAccessors {
  private boolean encapsulatedThing;

  class Inner {
    void doSomething() {
      encapsulatedThing = true;
    }
  }
}

上面的代码(使用Sun Java 6 javac编译)在SyntheticAccessors.class中创建了这些方法:
Compiled from "SyntheticAccessors.java"
public class demo.SyntheticAccessors extends java.lang.Object{
    public demo.SyntheticAccessors();
    static void access$0(demo.SyntheticAccessors, boolean);
}

注意新的access$0方法。

4
这种在代码中实现安全性的想法有点傻。如果您需要代码级别的安全,请使用混淆工具。就像@skaffman在上面的评论中所说:“代码可见性从来不是一项安全功能。即使是私有成员也可以使用反射访问。”。
如果您要分发编译好的代码而没有进行混淆,那么使用内部类是您最后需要担心的事情,如果您担心他人擅自干预您的私人内容。
如果您托管您的代码,那么为什么还担心有人研究您的内部类呢?
如果您将链接一些您不信任且无法在运行时检查的第三方代码,则需要对其进行沙箱处理。
正如我之前所说,如果这确实是贵公司的政策,请立即向thedailywtf.com报告贵公司。

1
我不理解你的评论:mR_fr0g公司的安全人员不希望私有字段变为公共字段,无论实现方式是否现实。混淆工具不会改变任何安全级别,每种获取访问私有字段的方法在混淆后仍将有效。 - Jerome
3
没错,这是一个糟糕的建议。混淆并不会使任何东西更加安全,只会意味着黑客需要做更多的工作。但最终,他们以前能做的任何事情仍然可以做到。 - Chad Okere
通过反射,你可以随意访问私有字段。 - nos
3
请注意,如果您没有“suppressAccessChecks”权限的反射许可,您将无法通过反射访问私有字段。 - Jerome
Jerome: 对,除非你在该类内部进行反射操作,否则这基本上是没有意义的。 - Tom Hawtin - tackline
-1 这完全是错的。通过模糊化来达到安全性并不能提供真正的安全保障。混淆代码是最后一招,你应该避免使用。而且正如所提到的,访问控制破坏是由 SecurityManager 处理的。 - Antimony

4

您应该考虑应用程序需要提供何种安全性。拥有安全架构的应用程序将不会遇到这些已命名的问题。

如果用户无权使用您的代码执行某些操作,则必须将此功能分离并在服务器上运行(用户无法访问类文件)。

请记住,您始终可以反编译Java类文件。不要依赖"安全性通过晦涩难懂实现"。即使混淆的代码也可以被分析、理解和修改。


4

恶意代码可以使用Java反射来获取JVM中的任何信息,除非设置了安全管理器以禁止此操作,这包括将私有字段更改为公共字段等。

我个人认为,不使用内部类的原因被其他可能性所淹没,因此如果需要并且易于阅读,则可以使用内部类。


3
"鉴于除了外部和内部类之外的任何类都无法访问外部类的私有成员,那么这实际上是否存在安全风险?"
即使在正常情况下它不会编译,您仍然可以生成自己的字节码。但这并不是避免使用内部类的理由。您需要做的就是假设所有内部类都是公共的。
如果您真的想运行不受信任的代码,请学习如何使用Java安全体系结构设置自己的沙箱和安全级别,这并不难。但大多数情况下,您应该避免在安全环境中运行随机代码。"

-1
请注意,所列出的缺点不适用于static内部类,因为它们没有对其封闭类(或对象)的隐式访问。
因此,如果这个规则将有助于您的公司,那么可以考虑豁免静态内部类,因为它们提供了一种封装方式,在许多情况下非常有用。
引用Java语言规范的Tom表示:“成员类可以是静态的,在这种情况下,它们无法访问周围类的实例变量”。

1
静态嵌套类可以私有访问封闭类(反之亦然)。 - Tom Hawtin - tackline

-1

胡说八道!按照同样的逻辑,也不要编写公共方法,因为它们可以访问私有字段,唠叨!


2
这不是语言问题。嵌套类可以访问封闭类的私有成员,反之亦然,这并不是讨论的问题。问题在于添加嵌套类可能会导致私有成员对同一包中注入的任何代码都可访问(如果恶意代码确实可以注入到该包中)。 - Tom Hawtin - tackline

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接