如何确保在 Java 中销毁 String 对象?
Posted
技术标签:
【中文标题】如何确保在 Java 中销毁 String 对象?【英文标题】:How can I ensure the destruction of a String object in Java? 【发布时间】:2011-07-11 10:07:56 【问题描述】:我公司的一名员工需要通过我制作的程序修改 SQL Server 数据库中的数据。该程序最初使用 Windows 身份验证,我要求 DBA 授予该特定用户对所述数据库的写入权限。
他们不愿意这样做,而是授予对 my Windows 用户帐户的写入权限。
由于我信任这个人,但不足以让他在我的会话打开的情况下工作 90 分钟,我将在我的程序中添加一个登录提示,要求输入用户名和密码组合,然后使用它登录 SQL Server .我会登录并相信我的应用程序会让他只做他需要做的事情。
但是,这会带来很小的安全风险。 SunOracle 网站上的 password fields tutorial 声明密码应在内存中保留所需的最短时间,为此,getPassword
方法返回一个 char[]
数组,您可以将其归零完成后。
但是,Java 的 DriverManager
类只接受 String
对象作为密码,所以我无法在完成密码后立即处理它。而且由于我的应用程序在分配和内存要求上偶然非常低,谁知道它会在内存中存活多久?如上所述,该程序将运行相当长的时间。
当然,我无法控制 SQL Server JDBC 类用我的密码做什么,但我希望我可以控制 我用我的密码做什么。
有没有一种万无一失的方法可以用 Java 销毁/清零 String
对象?我知道两者都有点反对语言(对象破坏是不确定的,String
对象是不可变的),System.gc()
也有点不可预测,但仍然;有什么想法吗?
【问题讨论】:
所以你假设一个邪恶的人可以偷看你的应用程序的内存,你想在那种情况下打败他?您正试图为不可能的威胁找到不可能的解决方案。作恶者有更简单的方法来获取密码,如果他有那种访问计算机的权限。唯一有效的解决办法是用一顶大锡箔帽把电脑屏蔽起来。 不言而喻,但我相信您知道许多公司对密码共享有严格的政策。当然,我们都会时不时地这样做——但持续地升级问题并设置您需要的帐户并不是更简单的途径。我想反思可能是采用的途径 - 正如其他人所回答的那样。 @irreputable 我不得不同意在这种情况下它并不重要(因为我已经对答案发表了评论),但如果我遇到更偏执的环境,这种情况可能再次发生。 现实吗?只有您了解您的用户。但如果我是你,我会提供一个远程接口。如果他可以访问正在运行您的应用程序的计算机,并且他那么足智多谋和坚定,那么您就是干杯。 @irreputable 在我的情况下这不现实,用户是个好人。问这个问题是否不合适? 【参考方案1】:所以,这是个坏消息。我很惊讶还没有人提到它。使用现代垃圾收集器,甚至整个 char[] 概念都被打破了。无论您使用 String 还是 char[],数据最终都会在内存中保存多久。这是为什么?因为现代 jvm 使用分代垃圾收集器,简而言之,将对象复制到所有地方。因此,即使您使用 char[],它使用的实际内存也可能会被复制到堆中的各个位置,到处留下密码副本(并且没有高性能 gc 会将旧内存归零)。因此,当您将最后拥有的实例清零时,您只是将内存中的最新版本清零。
长话短说,没有万无一失的方法来处理它。你几乎必须信任这个人。
【讨论】:
内存没有被清零的任何引用?毕竟,分配新对象时内存必须为零,以便所有字段都获得其初始值。我一直认为这可以通过在 GC 期间将内存归零来实现。 @meriton - 我记得不久前看到一篇文章详细介绍了一些与此相关的测试,但我上次看时找不到。至于将内存归零,并非所有内存使用都将是新的对象分配。例如,当现有对象从一代复制到另一代时,无需事先将目标归零。 嗨@jtahlborn,我是Java 新手,所以也许这是一个糟糕的问题:有没有办法暂停特定代码的垃圾收集。即告诉 JVM 在我的“身份验证”方法终止之前不要运行 GC?另外,由于答案是 9 年,它仍然是真的,对于查看进程内存的密码泄露没有很好的处理吗? @EliyahuMachluf - 是的,这个答案仍然相关。我不相信任何标准 jvm 允许您在特定时间段内暂停垃圾收集。我很确定一些针对嵌入式空间的 jvm 确实对 gc 有更多的控制权,所以如果一些利基 jvm 提供此功能,我不会感到惊讶。我仍然会重新审视你为什么认为你需要这个功能,因为我怀疑这个推理是否成立。【参考方案2】:我只能想到一个使用反射的解决方案。您可以使用反射来调用使用共享字符数组的私有构造函数:
char[] chars = 'a', 'b', 'c';
Constructor<String> con = String.class.getDeclaredConstructor(int.class, int.class, char[].class);
con.setAccessible(true);
String password = con.newInstance(0, chars.length, chars);
System.out.println(password);
//erase it
Arrays.fill(chars, '\0');
System.out.println(password);
编辑
对于任何认为这是一种防故障甚至有用的预防措施的人,我鼓励您阅读 jtahlborn's answer 至少一个警告。
【讨论】:
这不会阻止驱动程序使用传递的值new String( password )
创建一个副本这是一个JDBC驱动程序的问题。
@OscarRyz 没有什么可以反对的,所以我必须相信微软的人。
@Oscar:同意,但我真的不认为 OP 担心司机会做什么。我不会说这是驱动程序问题,我会说这是 API 问题。大概设计者认为,如果连接是通过不受信任的代码获得的,那么您无论如何都放弃了安全性。
+1 我认为最初的问题有点误导,但无论如何都很有趣。在此之后,我将确保创建我收到的字符串的副本 :)【参考方案3】:
如果您绝对必须,请保留对字符串的 WeakReference,并继续吞噬内存,直到您强制对字符串进行垃圾收集,您可以通过测试弱引用是否变为空来检测。这可能仍会将字节留在进程地址空间中。可能是垃圾收集器的更多搅动会给你带来安慰吗?所以在你的原始字符串弱引用被清空后,创建另一个弱引用并搅动直到它被清零,这意味着一个完整的垃圾回收周期已经完成。
不知何故,即使我上面的回答完全是认真的,我也必须为此添加 LOL :)
【讨论】:
顺便说一句,重新反射上面的答案,他们假设您正在运行的 JVM 具有具有这些私有/受保护成员的字符串的特定实现。【参考方案4】:您可以使用反射更改内部char[]
的值。
您必须小心使用相同长度的数组更改它,或者同时更新count
字段。如果您希望能够将其用作集合中的条目或映射中的值,则需要重新计算哈希码并设置hashCode
字段的值。
话虽如此,实现这一点的最少代码是
String password = "password";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] chars = (char[]) valueField.get(password);
chars[0] = Character.valueOf('h');
System.out.println(password);
【讨论】:
最小的代码不会“泄露”旧的 char 数组吗?我会遇到同样的问题:我的明文密码存在于内存中的某处。 关于问题 1 和 2。我相信除了 char[] 之外,String 中还有一个长度字段需要更新。只需拉起字符串源。 @Konstantin:更准确地说,应该更新 count 和 hash 以使字符串完全按预期工作。 现在,这是对安全方面的第一次严肃回应!虽然,认为 'Arrays.fill(chars, (char) 0);' 实际上会覆盖密码并授予一个干净的 String 实例。随后的“password = null;”将限制“hashCode”的不一致。【参考方案5】:我不确定DriverManager
课程。
一般来说,您是对的,建议将密码存储在 char 数组中,并在使用后明确清除内存。
最常见的例子:
KeyStore store = KeyStore. getInstance(KeyStore, getDefaultType()) ;
char[] password = new char[] 's','e','c','r','e','t';
store .load(is, password );
//After finished clear password!!!
Arrays. fill(password, '\u0000' ) ;
在 JSSE 和 JCA 中,设计正是考虑到了这一点。这就是 API 期望 char[]
而不是字符串的原因。
众所周知,字符串是不可变的,因此密码符合未来垃圾回收的条件,之后您无法重置密码。窥探内存区域的恶意程序可能会导致安全风险。
我认为在这种情况下,您正在寻找解决办法。
其实这里也有类似的问题:Why Driver Manager not use char arrays?
但没有明确的答案。
似乎这个概念是密码已经存储在属性文件中(已经有一个 DriverManager 构造函数接受属性),因此文件本身已经比实际将密码从文件加载到字符串中带来了更大的风险。
或者 API 的设计者对访问数据库的机器的安全性有一些假设。
我认为最安全的选择是尝试调查(如果可能)DriverManager 如何使用密码,即它是否保留了内部引用等。
【讨论】:
【参考方案6】:所以一旦我完成密码,我将无法处理它。
为什么不呢?
如果您通过
获得连接Driver.getConnection
你只需要传递密码并让它被 gc'ed。
void loginScreen()
...
connect( new String( passwordField.getPassword() ) );
...
...
void connect( String withPassword )
connection = DriverManager.getConnection( url, user, withPassword );
...
//<-- in this line there won't be any reference to your password anymore.
当控件从connect
方法返回时,您的密码将不再有引用。没有人可以再使用它了。如果您需要创建新会话,则必须使用新密码再次调用“连接”。对保存您信息的对象的引用丢失。
【讨论】:
Oracle 明确建议不要等到它被 GCed。JPasswordField
返回 char[]
的主要原因是字符数组中的数据是可变的,一旦你完成它们,你可以将字节清零。这些字节保留在内存中可能很危险,因为恶意软件有可能访问它。
但是如果你不保留对它的引用,它就不会被使用,而且你的密码是安全的!
如果恶意软件可以读取另一个进程的地址空间,那将是不安全的。他们不需要 Java 引用来访问原始内存。
在自己的JDBC驱动中?如果您不信任您的 JDBC 驱动程序来保存密码,请选择另一个,您无法控制它。
@Oscar:不一定是 JDBC 驱动程序。它可以是任何特权应用程序或在不安全操作系统上运行的任何应用程序。你永远不知道什么时候会被垃圾回收,仅仅因为垃圾回收并不意味着底层数据不存在于内存中。【参考方案7】:
如果字符串没有被 JDBC 驱动程序管理器保存(一个很大的如果),我不会担心强制它的破坏。即使有大量可用内存,现代 JVM 仍然可以相当迅速地运行垃圾收集。问题是垃圾收集是否是有效的“安全擦除”。我怀疑是这样。我猜它只是忘记了对该内存位置的引用,并且不会将任何内容归零。
【讨论】:
感谢您让我放心!哈哈 实际上,垃圾回收是相对安全的,至少在年轻代中是这样,因为它涉及在幸存者空间的两半之间不断复制数据,直到永久保存到老年代。无论如何,这不是非常可预测或可控的,只是一种好奇心。【参考方案8】:没有强制垃圾收集的常规方法。可以通过本机调用,但我不知道它是否适用于字符串,因为它们是池化的。
也许另一种方法会有所帮助?我对 SQL Server 不是很熟悉,但在 Oracle 中,做法是让用户 A 拥有数据库对象和一组存储过程,而用户 B 什么都不拥有,但对这些过程具有运行权限。这样密码就不再是问题了。
在您的情况下,您的用户将拥有所有数据库对象和存储过程,而您的员工将需要对存储过程的运行权限,希望 DBA 不太愿意提供这些权限。
【讨论】:
也不能说这是最好的方法,但无论如何这种情况是相当特殊的。也就是说,我并不真的担心会出什么问题,我只是想看看如果我不得不在更危险的环境中做其他类似的事情时如何解决这个问题。 @zneak 我想说的是,如果 DBA 不想给他/她写访问权限,那么你也不应该真的这样做。在某种程度上,这会破坏他们的工作。 他们完全了解情况,所以我不会破坏任何事情。这不是问题。 @zneak 是一个以 I 开头并以 SO 结尾的三个字母单词的原因吗? :) 恐怕这就是我所能提供的全部帮助了,我检查了 MSSQL 驱动程序是否具有特定于实现的密码传递方式,但我没有成功。【参考方案9】:有趣的问题。一些谷歌搜索揭示了这一点:http://securesoftware.blogspot.com/2009/01/java-security-why-not-to-use-string.html。根据评论,它不会有所作为。
如果您不将字符串存储在变量中而是通过 new String(char[]) 传递它会发生什么?
【讨论】:
感谢您的关注;但是,我的 conerns 具有安全性。如果某个进程以某种方式访问了我的程序的地址空间,他们可以在那里以纯文本形式找到我的密码。 MVC 不会解决这个问题。以上是关于如何确保在 Java 中销毁 String 对象?的主要内容,如果未能解决你的问题,请参考以下文章