h2 数据库从 1.x 版本升级到 2.x 版本排坑记录

353 阅读5分钟

背景

又是一次被漏洞追着升级的记录,排了一天升级的坑,照例是要记录一番的。漏扫到「H2 控制台 JNDI 远程代码执行漏洞(CVE-2021-42392)」,漏洞描述:

H2 是一个用 Java 编写的关系数据库管理系统。 H2 存在远程代码执行漏洞,该漏洞是由于 H2 控制台可以通过 JNDI 从远程服务器加载自定义类,攻击者可利用该漏洞在未授权的情况下,构造恶意请求,触发远程代码执行漏洞。

需要对 h2 进行升级,本文记录升级过程中的问题。

h2 的 2.x 版本和 1.x 版本相差有点大,主要有几个问题:

  1. h2 的版本选择,需要考虑 JDK 版本。
  2. v1.x 的数据库文件格式和 v2.x 的格式不兼容,必须走数据库文件的升级。
  3. v2.x 新增了很多关键字,旧版本能正常识别的字符可能是新版本的保留关键字。
  4. 包含 BLOB 类型的表,导入导出会出现数据超长问题,需要先舍弃再重新创建。

版本选择

本来想直接升到最新版本 2.3.232 ,启动发现 JDK 版本不兼容,这个版本是用 JDK11+ 编译的,而咱们的运行环境的 JDK 版本过低,所以保守选择漏洞范围外的最近版本 h2-2.0.206 。

梳理出来的升级方案:

  1. 用旧版本的 jar 执行旧版本数据库文件导出命令。
  2. 用新版本的 jar 执行新版本数据库导入命令,创建新版本的数据库文件。
  3. 停止旧版本的数据库进程。
  4. 用新版本启动数据库进程。

关键字排除

数据库导入/导出命令:

java -cp h2-1.4.197.jar org.h2.tools.Script -url "jdbc:h2:file:./my-data" -user $username -password $password -script backup.sql

java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./my-data-new" -user $username -password $password -script backup.sql

导入操作报了很多 expected "identifier";,根源是 H2 的 2.x 版本要求对保留的关键字使用双引号包围,还可以通过 jdbc url 配置 ;NON_KEYWORDS=KEYWORD1,KEYWORD2 对指定保留关键字进行排除。

对着 h2 1.x 版本导出的 SQL 文件,把所有可疑的关键字都摘出来后,调整导入语句如下:

java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./my-data-new;MODE=LEGACY;NON_KEYWORDS=USER,KEY,PATH,COMMAND,INTERVAL,VALUE,USERNAME,PASSWORD,UNIT,DAYTIME,HOUR" -user $username -password $password -script backup.sql

BLOB 表问题

排除关键字后,还有一个比较麻烦的问题。因为数据库中有一个表存储了应用上传的文件,导出 SQL 语句对它处理时创建了一个新表并为该表创建索引,这个语句导入会报错:

Exception in thread "main" org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement " \000aCREATE PRIMARY[*] KEY SYSTEM_LOB_STREAM_PRIMARY_KEY ON SYSTEM_LOB_STREAM(ID, PART)"; expected "OR, FORCE, VIEW, ALIAS, SEQUENCE, USER, TRIGGER, ROLE, SCHEMA, CONSTANT, DOMAIN, TYPE, DATATYPE, AGGREGATE, LINKED, MEMORY, CACHED, LOCAL, GLOBAL, TEMP, TEMPORARY, TABLE, SYNONYM, UNIQUE, HASH, SPATIAL, INDEX"; SQL statement:
 
CREATE PRIMARY KEY SYSTEM_LOB_STREAM_PRIMARY_KEY ON SYSTEM_LOB_STREAM(ID, PART) [42001-206]

怀疑是设置了 KEY 为非关键字导致的,所以修改 SQL 中 KEY 关键字加双引号后,导入还是报这个错误。

然后比对主键创建语句发现其他表的主键创建不是用 CREATE PRIMARY KEY 而是用 ALTER TABLE 添加的,改成一样的语法:

ALTER TABLE PUBLIC.SYSTEM_LOB_STREAM ADD CONSTRAINT PUBLIC.CONSTRAINT_6C0124 PRIMARY KEY(ID, PART);

结果导入还是报错 BDATA BINARY 列 Value too long:

image.png

导出命令只能针对整个数据库文件,没有单独排除某个表的方法。估计是这个表没法处理了,只能舍弃掉该表了。

升级文件准备

因为 BLOB 字段问题,放弃该表,采取迂回处理。

  1. 导出 1.x 版本的数据之前,先执行一个删除包含 BLOB 字段的表的操作。
  2. 导入 2.x 版本后再补充执行创建 BLOB 表的操作。

准备 h2 升级文件,清单如下:

  1. 新版本:h2-2.0.206.jar。
  2. 旧版本:h2-1.4.197.jar。
  3. 删除BLOB表的脚本:BLOB_TABLE_DROP.sql。
  4. 创建BLOB表的脚本:BLOB_TABLE_CREATE.sql。
  5. 旧版本的数据库文件。

将这些文件放在同一个目录下,直接用 jar 包中的导入、导出工具完成数据库升级。

升级脚本编写

#!/bin/sh
dir=$(dirname "$0")

# Step1:Prepare for upgrade.
appHome=`pwd`
echo 'home path is '$appHome

# Step2:Clear history db file and restart h2 with version 2.x.
cd $appHome/h2/
rm -rf app*
sh stop.sh
sh startH2ByV2.sh

# Step3:Upgrade h2 dt.mv.dv from v1.4.197 to v2.0.206 as file format is related to h2 version.
# 3.1 Get username and password ,need to trim return char ,otherwise will encounter「Wrong user name or password」error.
username=$(cat $appHome/conf/db.properties |grep username=|awk -F "=" '{print $2}'|tr -d ' \r')
password=$(cat $appHome/conf/db.properties |grep password=|awk -F "=" '{print $2}'|tr -d ' \r')

# 3.2 Copy old version db file to h2  directory.
cp $appHome/data/appDBData.mv.db .

# 3.3 Drop table with BLOB field and export use v1.4.197.
java -cp h2-1.4.197.jar org.h2.tools.RunScript -url "jdbc:h2:file:./appDBData" -user $username -password $password -script BLOB_TABLE_DROP.sql
java -cp h2-1.4.197.jar org.h2.tools.Script -url "jdbc:h2:file:./appDBData" -user $username -password $password -script app_backup.sql

# 3.4 Create dt-new use v2.0.206 and create table with BLOB field for new db.
java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./appDBData-new;MODE=LEGACY;NON_KEYWORDS=USER,KEY,PATH,COMMAND,INTERVAL,VALUE,USERNAME,PASSWORD,UNIT,DAYTIME,HOUR" -user $username -password $password -script app_backup.sql
java -cp h2-2.0.206.jar org.h2.tools.RunScript -url "jdbc:h2:file:./appDBData-new" -user $username -password $password -script BLOB_TABLE_CREATE.sql

# Step4:Update app's db file and restart.
# 4.1 Copy dt-new.mv.db to data directory.
cp appDBData-new.mv.db $appHome/data/

# 4.2 Update db.properties url with new db file.
cp $appHome/conf/db.properties $appHome/conf/db-h2-v1.properties
dtNew=$(cat $appHome/conf/db.properties|grep 'NON_KEYWORDS')
if [ -z $dtNew ]; then
    echo "Update db.properties use new database file."
    sed -i -c "s/data\/appDBData/data\/appDBData-new;NON_KEYWORDS=USER,KEY,PATH,COMMAND,INTERVAL,VALUE,USERNAME,PASSWORD,UNIT,DAYTIME,HOUR/" $appHome/conf/db.properties
else
    echo "Db.properties is already updated with new database file."
fi

# 4.3 Restart app.
cd $appHome/bin
sh app.sh restart

启示录

总体来看比较简单的,但时升级时截取应用配置的数据库帐密信息时遇到一个问题,就是通过脚本截取的信息一直报帐号或密码错误「Wrong user name or password」,而直接设置帐号和密码又能正确导入导出。

所以断定解析结果出了问题,果然是 awk -F 截取到的信息结尾有一个 CR 字符,该配置文件是 CRLF 的分隔符,awk 除了了 \n 还是保留了一个 CR 字符即 \r ,需要去掉才能得到正确的数据库信息。

修正获取认证信息的脚本,结尾加上 tr -d ' \r' 就可以了。

username=$(cat $appHome/conf/db.properties |grep username=|awk -F "=" '{print $2}'|tr -d ' \r')

还有一个启示就是,数据库建表时需要谨慎,起名的时候应该避开数据库的保留关键字。例如数据库有一个表的某个字段名称就是 KEY,这是明显的索引关键字呀。

此外,如果用原生 JDBC 操作数据库,应该用列序号来处理查询结果,避免直接用列名称,因为不同数据库列名称大小写有差异。比如 Oracle 默认大写,但是 Postgrel 默认小写,程序想兼容不同数据库,最好用 getString(columnIndex) 这个方法来获取查询结果。

2024年有半年是在排这个老项目的坑,一全栈开发硬是成了运维角色,一年了,没正经写过代码。甲辰年腊月二十六,我们腊月二十九才开始放假啊!