Java字符串不是真正的不可变对象,这会产生哪些影响?

6

背景

在Java 101中,我们学到:

String是不可变的。

是的。很好。谢谢。

然后我们进入Java 102(或者可能是Java 201),我们发现:

使用反射可以更改 String ,所以它并不是真正的不可变。

啊。好吧。这取决于您的观点,它可能非常可爱或无法衡量。

迄今为止,这些事情已经在Stack Overflow和其他地方被广泛讨论。在提出这个问题时,我认为这一点是公认的。

问题

当我们发现 String 并不是真正不可变时,对于我们如何编写代码有什么影响?

让我从两个方面具体化这个问题。

JVM沙箱

是否可能有恶意类利用此技巧来破坏JVM的安全性,并进行不应该的操作?例如,JDK中是否有方法返回 String ,只有在假定它不可更改时才是安全的?

这里有一个有希望的开端:

    String prop = "java.version";
    // retrieve a System property as a String
    String s = System.getProperty(prop);
    System.out.println(s);
    // now mess with it
    Field field = String.class.getDeclaredField("value");  
    field.setAccessible(true);  
    char[] value = (char[])field.get(s);  
    value[0] = 'x';
    //turns out we've changed not just the String we were
    //given but the underlying property too!
    System.out.println(System.getProperty(prop));

这段代码从JVM检索系统属性,以String返回,然后更改String;这样做的结果是基础属性也被更改了。这种方法会引起混乱吗?
我不确定。值得注意的是,您必须具有执行反射的权限。在这个点上,安全性是否已经受到威胁?它让我能够避免没有权限更改安全属性,但这是否可以预料,还是个问题?
有什么办法可以扩展这个方法来做一些更糟糕的事情吗?
防御性克隆
我们总是被告知,在传递给可能修改它们的方法之前,最好克隆数组和其他对象,除非我们希望它们被修改。这只是良好的编程实践。如果你把数组的唯一副本交给别人,然后它被弄乱了,那就是你自己的错误。
但看起来同样的论点也应该适用于一个String!我从未听说过任何人说我们应该在将String传递给其他人编写的方法之前进行克隆。但是,如果一个String与一个数组一样可变,这有什么不同呢?
支持防御性克隆的论点
如果防御性克隆只是为了不真正知道方法可能对我们传递的事物做什么,而且如果我们正在传递它是可变的,那么我们应该进行克隆。如果代码可能是恶意的,则可能会更改String;因此,每当重要的是String保持不变时,我们都应该确保发送副本而不是真实的东西。
不能说不信任的代码应该在安全管理器下运行,如果我们不相信它不会对String做坏事。如果对于一个String来说是这样的话,那么对于一个数组也是一样的;但没有人说只有在将代码与安全管理器锁定的情况下才需要克隆数组和其他对象。
反对防御性克隆的论点
一个数组可能会被粗心的编码修改;换句话说,它可能会被无意地修改。但没有人会无意中更改String:只能通过狡猾的技巧来完成,这意味着程序员试图打破不可变性。
那就区分了两种情况。我们绝对不能把一个数组(即使是克隆后的)传递给我们完全不信任的代码。你不可能从某个不靠谱的地方下载一个库,然后认为自己已经安全,因为你克隆了自己的数组。你之所以要守卫克隆操作是因为你认为程序员可能会犯错误,或者对于你发送的数据所允许的内容有不同的假设。如果你担心代码可能会背着你修改 "String",那么你根本不应该运行这段代码。你只是增加了性能开销,通过克隆 "String" 所达到的目的根本毫无意义。

答案是什么?

我们应该如何思考这个问题?这些技巧可以打破JVM沙盒吗?我们是否应该在考虑到 String 的可变性时进行防御性编码,还是这只是一个转移注意力的话题?


2
答案是:不要允许在您的应用程序中使用反射,至少不要在系统对象上使用。 (任何允许不受信任的代码在安全应用程序中使用反射的人都是傻瓜。) - Hot Licks
Java有一个安全管理器,你要么a)根据你的编码标准防止开发人员执行不安全操作,要么b)在没有安全管理器的情况下不应该运行你不信任的代码,或者更好的办法是,无论是内部编写的还是外部编写的,不要运行你不信任的代码。 - Peter Lawrey
在一个“安全”的应用程序中,我仍然可以要求您提供密码、信用卡详细信息、母亲的婚姓等信息,有些人可能会输入这些详细信息。 - Peter Lawrey
在沙盒选项方面,您可以考虑使用Docker。这为您提供了Java没有的控制选项,例如限制CPU使用等。 - Peter Lawrey
你不应该从可疑的地方下载库,然后认为你是安全的,因为你克隆了你的数组。通常情况下,库比使用它的代码更值得信任。在这种情况下,你可能需要复制(而不是通常的“克隆”)输入和输出的[不适当设计的]值类型。 - Tom Hawtin - tackline
2个回答

4
有一种安全设置可以为您的Java程序启用。根据“setAccessible”的Javadocs
首先,如果有安全管理器,则使用ReflectPermission(“suppressAccessChecks”)权限调用其checkPermission方法。
如果标志为true但是无法更改此对象的可访问性(例如,如果此元素对象是类Class的Constructor对象),则会引发SecurityException。
如果此对象是类java.lang.Class的Constructor对象,并且flag为true,则会引发SecurityException。
因此,通过不允许此检查的SecurityManager,您可以防止任何代码成功调用setAccessible,保留String的不可变性。

3

Java有机制可以防止这种情况发生。例如,为了破坏String,您必须调用field.setAccessible(true);,这对您有效,但如果您查看文档,该setAccessible(true)调用可能会抛出SecurityException

Java允许您安装或修改安全管理器,以锁定这些反射API。例如,考虑允许您运行受限Java应用程序的Ideone。在很大程度上,他们通过限制系统的安全权限来实现这一点。

您应该阅读SecuityManager以及trail document的相关内容。

您还应该仔细阅读以下内容:


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