阅读 101

native 方法改变入参导致 BLOB 数据存储不一致问题复盘

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

背景

上周被一个问题纠缠了四天,终于在昨天吃饭的时候顿悟到了问题的症结。这个 bug ,差点都让我怀疑 MyBatis 框架处理二进制类型的数据是不是有漏洞?

事实证明,人家框架是健壮、好用的,是我犯了一个低级错误啊。

问题描述:某个应用系统的证书文件以 BLOB 类型存储到数据库中,证书文件的解密定义为 native 方法,由 C 语言实现。据说是为了增加爆破难度,确实, C 语言的 .so 文件反编译还是有点门槛的。

证书上传功能,上传加密的证书后调用 JNI 方法校验。验证通过,再调用基于 MyBatis BaseMapper 实现的 DAO 方法, insert 或者 update ,存储证书文件的二进制流到数据库。

诡异的是:更新或者插入证书文件后,再次读取文件二进制数据,竟然跟上传的内容不一样,导致调用 JNI 方法解密失败,反复测试都是如此。

追踪过程

证书验证的方法是一个 JNI ,入参是加密证书文件的二进制流,返回证书各项信息的 Map 对象:

public native Map<String, String> check(byte[] data) throws Throwable;
复制代码

证书文件 DAO 类最初定义为 BaseMapper 的子类,所有功能使用默认方法:

@Mapper
public interface MyFileDao extends BaseMapper<MyFile> {
}
复制代码

插入后立即读取,二进制数据流不一致,所以怀疑 MyBatis 的默认方法对 byte[] 类型的字段处理有问题。于是重新实现了该类,自定义增、改 SQL 语句,并指定 JDBC 类型为 BLOB

<insert id="insert" parameterType="XX.MyFile">
  insert into XX(ID, FILE, MODIFY_TIME)
  values (#{id,jdbcType=DECIMAL}, #{file,jdbcType=BLOB},#{modifyTime,jdbcType=DECIMAL})
</insert>
复制代码

一番操作后,涛声依旧。

接着,单独写一个 insertupdate 文件的代码,又能正常存储。对比证书上传和独立调用的 DAO 调用逻辑,完全一样啊。再看二者执行过程中 SQL 语句,也都完全一样,实在找不到问题所在哇。

问题症结

反复验证了一下午,上传操作插入和查询的二进制数据流始终不一致,怀疑的焦点一直在入库的时候,数据是不是被 MyBatis 框架弄丢失了?

吃晚饭的时候,突然意识到传入 JNI 方法的是个二进制数组,入库操作也是同一个 byte[]对象,难道字节数组被 JNI 方法改变了?立马验证,Eureka ,果然是它!

JNI 是 C 语言组的同事写的,不知道里面是怎么实现的,也没想到方法内部会修改这个数组啊。最终我推测,C 实现的时候就地用了这个 byte[] 对象存储解密后的内容,于是直接打印 new String( byte[]) 数据库查到的数据,真是证书的明文信息。

问题修正:数据经过 JNI 后会变化,所以调用 JNI 之前先拷贝一份数据,插入或更新时用备份数据就好了。

byte[] copyData = new byte[myFile.length];
System.arraycopy(myFile,0,copyData,0,myFile.length);
复制代码

延伸分析:byte[] 类型在 DAO 中的类型

包含 byte[] 类型的表实体,执行默认的 updateinsert 时,类型是字节。 在这里插入图片描述 自定义插入函数指定 BLOB 类型:

targetFile,jdbcType=BLOB
复制代码

执行 SQL 中的类型变成了 ByteArrayInputStream在这里插入图片描述 无论哪种类型,对最终插入二进制流信息到数据库都没有影响。

启示录

这次碰到的 bug 纯粹是忘记了 Java OOP 基本特性导致的,插入之前和插入之后对象数据不一样,说明对象信息被改变了,而我却一直怀疑人家框架有问题。

对象是引用,不同的地方操作同一个地址,操作的就是同一份数据。

区分不同场景:数据需要共享,直接操作对象 OK 。不希望直接操作对象,最好用拷贝。

文章分类
后端
文章标签