Java反射机制清空字符串导致业务异常分析

描述

编者按笔者在处理业务线问题时遇到接口返回的内容和实际内容不一致的现象。根因是业务方通过Java反射机制将String类型敏感数据引用的value数组元素全部设置为'0',从而实现清空用户敏感数据的功能。这种清空用户敏感数据的方法会将字符串常量池相应地址的内容修改,进而导致所有指向该地址的引用的内容和实际值不一致的现象。

背景知识

JVM为了提高性能和减少内存开销,在实例化字符串常量时进行了优化。JVM在Java堆上开辟了一个字符串常量池空间(StringTable),JVM通过ldc指令加载字符串常量时会调用 StringTable::intern 函数将字符串加入到字符串常量池中。

  • 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 函数处理流程  

     

    JVM

 
  • 字符串的创建方式

    根据StringTable::intern函数处理流程,我们可以简单描绘如下6种常见的字符串的创建方式以及引用关系。

JVM


		

现象

某业务线使用fastjson实现Java对象序列化功能,低概率出现接口返回的JSON数据的某个属性值和实际值不一致的现象。正确的属性值应该为"null",实际属性值却为"0000"。

原因分析

为了排除fastjson自身的嫌疑,我们将其替换jackson后,依然会低概率出现同样的现象。由于两个不同三方件同时存在这个问题的可能性不大,为此我们暂时排除fastjson引入该问题的可能性。为了找到该问题的根因,我们在环境中开启远程调试功能。待问题复现,调试代码时我们发现只要是指向"null"的引用,显示的内容全部变成"0000",由此我们初步怀疑字符串常量池中的"null"被修改成"0000"。

一般导致常量池被修改有两种可能性:

  1. 第三方动态库引入的bug导致字符串常量池内容被修改;
  2. 在业务代码中通过Java反射机制主动修改字符串常量池内容;

业务方排查项目中使用到的第三方动态库,未发现可疑的动态库,排除第一种可能性。排查业务代码中使用到Java反射的功能,发现清空密码功能会使用到Java反射机制,并且将String类型密码的value数组元素全部设置为'0'。

业务出现的现象可以简单通过代码模拟:

  1. 在TestString对象类中定义一个nullStr属性,初始值为"null";
  2. 定义一个带有password属性的User类;
  3. 在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());
    }
}

复现代码字符串引用关系如下图所示。

JVM

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反射机制清空敏感数据。

后记

如果遇到相关技术问题(包括不限于毕昇 JDK),可以通过 Compiler SIG 求助。Compiler SIG 每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM 和 JDK 等相关编译技术,感兴趣的同学可以添加如下微信小助手入群。

 

审核编辑 :李倩


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分