Open7

Jarファイルから安全にクラスをロードしたい

なおちきなおちき

Jarから安全にクラスをロードしたいと思ったがSecurityManagerは削除予定だそうだ。(今更)
https://inside.java/2021/04/23/security-and-sandboxing-post-securitymanager/
に、SecurityManager廃止後のセキュリティについて書いてあるのでこれを読んでみる。

なおちきなおちき

ASMライブラリとカスタムクラスローダーを使用すればクラスファイルを解析し、特定のクラスを使用していればブロックすることができるそうなので今回はその方法でやってみる。

なおちきなおちき

ASMを依存関係に追加し、
ClassVisitorを継承したクラスを実装する。

class SecureClassChecker : ClassVisitor(ASM5) {
    var native = false
        private set
    var file = false
        private set

    override fun visitMethod(
        access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?
    ): MethodVisitor {
        //メゾットの定義と中身を確認
        //例 ネイティブな呼び出しをチェック
        if (access and ACC_NATIVE > 0){
            native = true
        }
        return object : MethodVisitor(ASM5) {
            override fun visitMethodInsn(
                opcode: Int, owner: String, name: String?, descriptor: String?, isInterface: Boolean
            ) {
                //メゾットの呼び出しを確認
                //例 java.io.Fileのメゾットの使用をチェック
                if (owner == Type.getInternalName(File::class.java)){
                    file = true
                }
                super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
            }
        }
    }
}

フィールドにはvisitFieldvisitFieldInsnなどが使用できる。詳しくはClassVisitor (ASM 9.4)を参考にして頂きたい。

なおちきなおちき

続いてClassLoaderを実装する。

class CustomJarClassLoader(private val jarFile: File, parent: ClassLoader = getSystemClassLoader()) : ClassLoader(parent) {
    @Throws(ClassNotFoundException::class)
    override fun findClass(name: String): Class<*> {
        try {
            val path = name.replace('.', '/') + ".class";
            val jarInputStream = URL("jar:" + jarFile.toURI().toURL() + "!/" + path).openStream()
            val allClassBytes: ByteArray = jarInputStream.readAllBytes()
            val classReader = ClassReader(allClassBytes)
            val secureClassChecker = SecureClassChecker()
            // クラスにデバッグ情報がある場合、それを無視する
            classReader.accept(
                secureClassChecker, ClassReader.SKIP_DEBUG
            )

            if (secureClassChecker.file||secureClassChecker.native) {
                throw ClassNotFoundException(
                    "Class cannot be loaded - contains illegal code"
                )
            } else {
                return defineClass(
                    null, allClassBytes, 0,
                    allClassBytes.size
                )
            }
        } catch (e: IOException) {
            throw ClassNotFoundException(
                "Error finding and opening class", e
            )
        }
    }
}

この場合、読み込んだコードにネイティブメゾット又はjava.io.Fileの関数を呼び出しているものがあった場合、ClassNotFoundExceptionでクラスのロードに失敗する。

なおちきなおちき
val clazz = CustomJarClassLoader(File("Plugin.jar")).loadClass("Main")
val instance = clazz.getConstructor().newInstance()

のようにして使うことができる。

なおちきなおちき

ただ、これだけで安全と言えるかどうか怪しいので他の方法も探していく