本文分享自华为云社区《Java反射机制清空字符串导致业务异常分析》,作者:毕昇小助手。
编者按:笔者在处理业务线问题时遇到接口返回的内容和实际内容不一致的现象。根因是业务方通过 Java 反射机制将 String 类型敏感数据引用的 value 数组元素全部设置为’0’,从而实现清空用户敏感数据的功能。这种清空用户敏感数据的方法会将字符串常量池相应地址的内容修改,进而导致所有指向该地址的引用的内容和实际值不一致的现象。
背景知识
JVM 为了提高性能和减少内存开销,在实例化字符串常量时进行了优化。JVM 在 Java 堆上开辟了一个字符串常量池空间(StringTable),JVM 通过 ldc 指令加载字符串常量时会调用 StringTable::intern 函数将字符串加入到字符串常量池中。
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
unsigned int hashValue = hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop found_string = the_table()->lookup(index, name, len, hashValue);
// Found
if (found_string != NULL) {
ensure_string_alive(found_string);
return found_string;
}
debug_only(StableMemoryChecker smc(name, len * sizeof(name[0])));
assert(!Universe::heap()->is_in_reserved(name),
"proposed name of symbol must be stable");
Handle string;
// try to reuse the string if possible
if (!string_or_null.is_null()) {
string = string_or_null;
} else {
string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
}
#if INCLUDE_ALL_GCS
if (G1StringDedup::is_enabled()) {
// Deduplicate the string before it is interned. Note that we should never
// deduplicate a string after it has been interned. Doing so will counteract
// compiler optimizations done on e.g. interned string literals.
G1StringDedup::deduplicate(string());
}
#endif
// Grab the StringTable_lock before getting the_table() because it could
// change at safepoint.
oop added_or_found;
{
MutexLocker ml(StringTable_lock, THREAD);
// Otherwise, add to symbol to table
added_or_found = the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
ensure_string_alive(added_or_found);
return added_or_found;
}
复制代码
根据 StringTable::intern 函数处理流程,我们可以简单描绘如下 6 种常见的字符串的创建方式以及引用关系。
现象
某业务线使用 fastjson 实现 Java 对象序列化功能,低概率出现接口返回的 JSON 数据的某个属性值和实际值不一致的现象。正确的属性值应该为"null",实际属性值却为"0000"。
原因分析
为了排除 fastjson 自身的嫌疑,我们将其替换 jackson 后,依然会低概率出现同样的现象。由于两个不同三方件同时存在这个问题的可能性不大,为此我们暂时排除 fastjson 引入该问题的可能性。为了找到该问题的根因,我们在环境中开启远程调试功能。待问题复现,调试代码时我们发现只要是指向"null"的引用,显示的内容全部变成"0000",由此我们初步怀疑字符串常量池中的"null"被修改成"0000"。
一般导致常量池被修改有两种可能性:
第三方动态库引入的 bug 导致字符串常量池内容被修改;
在业务代码中通过 Java 反射机制主动修改字符串常量池内容;
业务方排查项目中使用到的第三方动态库,未发现可疑的动态库,排除第一种可能性。排查业务代码中使用到 Java 反射的功能,发现清空密码功能会使用到 Java 反射机制,并且将 String 类型密码的 value 数组元素全部设置为’0’。
业务出现的现象可以简单通过代码模拟:
在 TestString 对象类中定义一个 nullStr 属性,初始值为"null";
定义一个带有 password 属性的 User 类;
在 main 方法中创建一个密码为"null"的 User 对象,使用 Java 反射机制将密码字符串的所有字符全部修改为’0’,分别在密码修改前后打印 TestString 对象 nullStr 属性值;
复现代码
import java.lang.reflect.Field;
import java.util.Arrays;
public class TestString {
private String nullStr = "null";
public String getNullStr() {
return nullStr;
}
static class User {
private final String password;
User(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
private static void clearPassword(User user) throws Exception {
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] chars = (char[]) field.get(user.getPassword());
Arrays.fill(chars, '0');
}
public static void main(String[] args) throws Exception {
User user = new User("null");
TestString testString = new TestString();
System.out.println("before clear password >>>>");
System.out.println(" User.password:" + user.getPassword());
System.out.println("TestString.nullStr:" + testString.getNullStr());
System.out.println("--------------------------------");
clearPassword(user);
System.out.println("after clear password >>>>");
System.out.println(" User.password:" + user.getPassword());
System.out.println("TestString.nullStr:" + testString.getNullStr());
}
}
复制代码
复现代码字符串引用关系如下图所示。
User 对象的 password 属性和 TestString 的 nullStr 属性引用都同时指向常量池中的"null"字符串,"null"字符串的 value 指向 {‘n’,‘u’,‘l’,‘l’} char 数组。使用 Java 反射机制将 User 对象的 password 属性引用的 value 数组全部设置为’0’,导致 TestString 的 nullStr 属性值也变成了 “0000”。
输出结果如下:
before clear password >>>>
User.password:null
TestString.nullStr:null
--------------------------------
after clear password >>>>
User.password:0000
TestString.nullStr:0000
复制代码
通过输出结果我们可以发现在通过 Java 反射机制修改某一个字符串内容后,所有指向原字符串的引用的内容全部变成修改后的内容。
总结
在保存业务敏感数据时避免使用 String 类型保存,建议使用 byte[]或 char[]数组保存,然后通过 Java 反射机制清空敏感数据。
点击关注,第一时间了解华为云新鲜技术~
评论