为什么公共静态不可变数组是安全漏洞?

42

《Effective Java》中提到:

// 潜在的安全漏洞!

static public final Thing[] VALUES = { ... };

有人能告诉我这个安全漏洞是什么吗?


如果您可以读源代码和/或字节码,那么它不比私有更安全。 - Amber
1
关键词是“潜力”!类中所有公开访问的内容必须被理解和有意义。这是一个_合同_!就像任何合同一样,它应该经过深思熟虑,清晰易懂,并且在所有情况下的使用和实现都是可接受的,因为不可避免地会尝试任何和所有场景。创建任何类的关键是从最严格的接口开始。几乎总是可以公开更多功能,但几乎总是很难删除它们。 - nicerobot
8个回答

58

声明static final public字段通常是类常量的标志。对于原始类型(如int、double等)和不可变类(例如字符串和java.awt.Color),这是完全可以的。然而,对于数组而言,问题在于即使数组引用是常量,数组元素仍然可以被更改,并且由于它是一个字段,更改是不可防范、不受控制和通常不受欢迎的。

为了解决这个问题,可以将数组字段的可见性限制为私有或包私有,这样在寻找可疑修改时需要考虑的代码体积较小。另一种选择是完全放弃数组,使用“List”或其他适当的集合类型。通过使用集合,您可以控制是否允许更新,因为所有更新都通过方法进行。您可以通过使用Collections.unmodifiableList()来包装集合以防止更新。但是请注意,即使集合是不可变的,您还必须确保存储在其中的类型也是不可变的,否则假定为常量的对象将面临意外更改的风险。


2
Java 中缺乏不可变数组是我最不喜欢的事情之一。 - Andy

48

要理解为什么这是一个潜在的安全漏洞而不仅仅是差的封装,请考虑以下示例:

public class SafeSites {
    // a trusted class with permission to create network connections
    public static final String[] ALLOWED_URLS = new String[] {
        "http://amazon.com", "http://cnn.com"};

    // this method allows untrusted code to connect to allowed sites (only)
    public static void doRequest(String url) {
        for (String allowed : ALLOWED_URLS) {
            if (url.equals(allowed)) {
                 // send a request ...
            }
        }
    }
}

public class Untrusted {
     // An untrusted class that is executed in a security sandbox.

     public void naughtyBoy() {
         SafeSites.ALLOWED_URLS[0] = "http://myporn.com";
         SafeSites.doRequest("http://myporn.com");
     }
}

正如你所看到的,错误地使用final数组意味着不受信任的代码可以破坏受信任代码/沙盒试图实施的限制。在这种情况下,这显然是一个安全问题。

如果你的代码不是安全关键应用程序的一部分,那么你可以忽略这个问题。但我认为这是一个不好的想法。将来你(或其他人)可能会在安全是一个问题的上下文中重用你的代码。无论如何,这就是作者称公共final数组为安全问题的原因。


Amber在评论中说了这句话:

如果你能以任何方式读取源代码和/或字节码,那么与私有没有什么区别...

这不是真的。

"坏人"可以使用源代码/字节码确定一个私有存在并且引用一个数组的事实不足以破坏安全性。坏人还必须将代码注入具有所需权限以使用反射的JVM中。这个权限在(正确实现的)安全沙箱中不可用于不受信任的代码运行。


3
顽皮小子 :D,是一个不错的函数名称。 - peeyush
2
我在会议上因为我的Porn大声笑了出来,谢谢。 - JavierIEH

16
注意,非空长度的数组始终是可变的,因此一个类拥有一个公共的静态final数组字段或者返回这样一个字段的访问器是错误的。如果一个类拥有这样的字段或者访问器,客户端将能够修改数组的内容。 --《Effective Java》第二版(第70页)

1
这是正确的答案;问题在于类型给人一种常量的印象,但它并不是常量。 - 00prometheus

2

一个外部类可以修改数组的内容,这可能不是您希望类的用户做的(您希望他们通过方法来完成)。听起来作者的意思是这违反了封装性,而不是安全性。

我猜想,声明此行的某人可能认为其他类无法修改数组内容,因为它被标记为final,但这是不正确的,final只阻止您重新分配属性。


2
如果您在沙盒中运行不受信任的代码,并且public static final String[]位于受信任的代码中,则这是一个安全问题。我认为作者的意思就是他所写的! - Stephen C
@Stephen C:如果一个人的意图是拥有一个内容不可变的数组,那么这确实是一个安全问题。如果程序员故意这样做,那么让任何人修改数组的内容就是数组接口的一部分。前者可能是一个安全问题,后者可能不是。或者我在这里漏掉了什么? - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
我认为作者的观点是,这是一个潜在的安全漏洞,因为设计/安全分析需要什么并不清楚。(我本来想说“程序员的意图是什么”,但我意识到程序员的意图是次要的。考虑以下可能性:1)程序员根本没有考虑这个问题,对此没有明确的想法;或者2)程序员故意这样做以引入安全漏洞。) - Stephen C

1
我还想补充一下Joshua Bloch在《Effective Java第三版》中提出的建议。 当然,如果数组声明为以下形式,我们可以轻松地更改其值:
public static final String[] VALUES = { "a", "b" }; 

a.VALUES[0] = "changed value on index 0";
System.out.println(String.format("Result: %s", a.VALUES[0]));

我们得到了 结果:在索引0上更改了值

Joshua Bloch 建议返回数组的副本:

private static final String[] VALUES = { "a", "b" };   
public static final String[] values()
{
    return VALUES.clone();
}

所以现在我们尝试:

a.values()[0] = "changed value on index 0";
System.out.println(String.format("Result: %s", a.values()[0]));

我们得到了结果:a,这正是我们想要实现的 - VALUES是不可变的。同时,将原始值、字符串或其他不可变对象声明为public static final也没有什么不好,例如:public static final int ERROR_CODE = 59;

1
在这个声明中,客户端可以修改Thing [0],Thing [1]等(即数组中的元素)。

0

我认为这只是关于公共和私有的整体问题。最好的做法是将局部变量声明为私有,然后使用get和set方法,而不是直接访问它们。这样可以使它们在程序外部更难被篡改。就我所知,就是这些。


3
比那更微妙。一个public static final String是可以的,但一个public static final String[]不行,因为数组的内容可以被改变。 - Stephen C

0

因为final关键字只保证引用值(例如将其视为内存位置),而不是其中的内容。


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