はじめに
とあるWebアプリ(サーバー・サイドJava)で使用しているライブラリー(jarファイル)に脆弱性が見つかり、対策が必要になりました。ライブラリーをバージョンアップすれば脆弱性は排除できるのですが、アプリ本体を改修しなければならなくなるので対応コストの観点からそれはできません。調査したところ、脆弱性の原因となっている機能は現時点では使用していなかったのですが、リスクは排除するようにとのセキュリティー・チームからの勧告に従って対応を検討することになりました。タイトルのとおり「Javassistでを使って脆弱性に蓋をする」方式でいけそうなことは確認できたのですが、Webアプリ由来の注意点もあったので備忘録として記録を残しておきたいと思います。
前提条件
- アプリはJava EE 7のWebアプリケーション
- 脆弱性が見つかったライブラリーはwarのlibに配置されている
- 脆弱性の原因となっているクラスはcom.abcxyz.product.HelloUtilで、sayHelloメソッドを利用することでセキュリティー上の問題が発生する可能性がある(前記クラス名、メソッド名は説明のための仮称です)。
要件
- 前記HelloUtil#sayHelloをコールしても機能しないよう無効化したい。
- もしコールした場合はRuntimeExceptionをスローしたい。
対応策
Webアプリ起動時の初期化処理で、Javassistを使ってHelloUtil#sayHelloを書き換えて、RuntimeExceptionをスローするように変更する。
実装コード
初期化処理のためのServletContainerInitializerを作成し、onStartupメソッドを実装します。
onStartupメソッドで、HelloUtilクラスのsayHelloメソッドの冒頭に例外をスローするコードを挿入しています。Javassistによる実装方法としては一般的なものですが、Webアプリ環境でJavassistを利用する場合に躓きそうなポイントが2点あったのでポイント1とポイント2としてコメントに記載しました。
package com.mycompany;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
public class MyInitializer implements ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
ClassPool cp = ClassPool.getDefault();
CtClass cc = null;
CtMethod m = null;
// ポイント1:war/libに配置したjarからクラスをロードするためのクラス・パスを追加
// これをしないとcp.get("com.abcxyz.product.HelloUtil")でNotFoundExceptionになる。
cp.insertClassPath(new LoaderClassPath(this.getClass().getClassLoader()));
try {
cc = cp.get("com.abcxyz.product.HelloUtil");
} catch (NotFoundException e) {
e.printStackTrace();
//例外処理の詳細は割愛
}
try {
m = cc.getDeclaredMethod("sayHello");
m.insertBefore("throw new RuntimeException(\"DON'T USE THIS METHOD.\");");
} catch (NotFoundException | CannotCompileException e) {
e.printStackTrace();
//例外処理の詳細は割愛
}
try {
//ポイント2:cc.toClassの第一引数にクラスローダーを明示的に指定
//引数なしでコールすると異なるクラスローダーが使われるのでメソッドの変更が有効にならない
cc.toClass(this.getClass().getClassLoader(), null);
} catch (CannotCompileException e) {
e.printStackTrace();
//例外処理の詳細は割愛
}
}
}
上記をコンパイルしたMyInitializer.class
とMyInitializerクラスのFQCNを記述したMETA-INF/services/javax.servlet.ServletContainerInitializer
を収録したjarファイルを作成します。MyInitializerを収録したjarの構成イメージは以下のようになります。jar名は何でもよい。
com
|
+---mycompany
|
+---MyInitializer
META-INF
|
+---services
|
+---javax.servlet.ServletContainerInitializer
com.mycompany.MyInitializer
上記で作成したjar(仮称:init_mywebapp.jar)をWebアプリの(war)のlibに収録します。
Webアプリ(war)を起動すると、MyInitializarによってHelloUtil#sayHelloが書き換えられ、sayHelloメソッドをコールすると以下のように例外が発生するようになります。
java.lang.RuntimeException: DON'T USE THIS METHOD.
at com.abcxyz.product.HelloUtil.sayHello(HelloUtil.java)
at com.mycompany.mywebapp.HelloServlet.doGet(HelloServlet.java:34)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:686)
at [internal classes]
動作確認に使用したソフトウェアとバージョン
ソフトウェア | バージョン | 備考 |
---|---|---|
Windows 11 Enterprise | 22H2 | |
IBM WebSphere Liberty | 22.0.0.11 | |
IBM Java | 1.8.0_331 | |
Javassist | 3.29.2-GA | Javassistサイトはこちら |
以上です。