Skip to main content

Jackson ObjectMapperにおけるJavaのJSONデシリアライズの問題

Blog Feature Java deserialize

2021年12月1日

0 分で読めます

以前のブログ記事では、Java独自のシリアライズプラットフォームと、そのセキュリティ上の影響について取り上げました。また最近では、Java 17の改善によって安全でないデシリアライズを防ぐ方法についても解説しました。しかし現在では、Java独自のシリアライズに頼る人は少なく、代わりにJSONが使われています。JSONはデータのシリアライズに最も広く使われている形式で、人間が読みやすく、Javaに限定されません。

最も広く使われているライブラリの1つがjackson-databindです。このライブラリは、オブジェクトをJSONに変換したり、その逆の変換を行ったりするためのObjectMapperを提供します。

広く使われているライブラリだからこそ、jackson-databindライブラリではここ数年、多くの脆弱性が報告されていることを知っておくことが重要です。ただし、Jackson ObjectMapperを使うこと自体が、必ずしもセキュリティリスクになるわけではありません。この記事では、Jacksonのデシリアライズ脆弱性の仕組みと、影響を受けないための対策を解説します。

詳しく見ていく前に、よくある質問にいくつかお答えします。

デシリアライズとは?

デシリアライズとは、ファイルやストリームのデータをアプリケーションで使用できるオブジェクトに変換することです。データはバイナリ形式の場合もあれば、JSONやXMLのような構造化データの場合もあります。デシリアライズは、オブジェクトをバイトストリームや構造化テキストに変換するシリアライズの反対の処理です。

Jackson ObjectMapperは何に使われますか?

Jackson ObjectMapperはJacksonのdatabindライブラリの一部で、JSONをJavaオブジェクトに変換したり、その逆の変換を行ったりするために使われます。JavaエコシステムでJSONとJavaを相互変換するために最も広く使われ、よく知られているライブラリの1つで、Spring Bootに標準で含まれています。

Jackson ObjectMapperは安全ですか?

はい。Jackson ObjectMapperは安全で、セキュアなデフォルト設定を使用しています。既知のセキュリティ問題を避けるため、常に最新バージョンに更新することが重要です。

ObjectMapperの作成にはコストがかかりますか?

ObjectMapperの作成にはかなりのコストがかかります。そのため、ObjectMapperのインスタンスは再利用することをおすすめします。Jackson ObjectMapperはスレッドセーフなので、安全に再利用できます。

Jackson ObjectMapperを使ってJSONからJavaオブジェクトを作成する

以下のコードでは、ObjectMapperを作成し、ファイルから読み込んだJSON文字列を使ってPersonを復元します。

ObjectMapper om = new ObjectMapper();
Person myvalue = om.readValue(Files.readAllBytes(Paths.get("person.json")), Person.class);
System.out.println("name:"+myvalue.name+" \n"+"Age:"+myvalue.age);

これは単純な処理で、特別なことは何も起こりません。アノテーションがない場合、Jackson ObjectMapperはリフレクションを使ってPOJOのマッピングを行います。リフレクションを使うため、アクセス修飾子にかかわらずすべてのフィールドで動作します。ゲッターとセッターがある場合は、それらを使ってマッピングします。

Jacksonのデフォルトタイピング

Jacksonライブラリを使ったJSONのシリアライズに関する脆弱性の多くは、デフォルトタイピングに関係しています。デフォルトタイピングは、デフォルトでは有効になっていません。明示的に有効にする必要があります。そのため、ほとんどの場合、このリストに挙げられている脆弱性がシステムに直接影響することはありません。

ここで、デフォルトタイピングとは何か、何に使うのかを説明します。

デフォルトタイピングは、ポリモーフィック型や継承を扱うためのJackson ObjectMapperの仕組みです。JSONをJava POJOにデシリアライズしたいものの、オブジェクトやフィールドのサブタイプがわからない場合は、スーパークラスにデシリアライズできます。

CoffeeとTeaというクラスがあるとします。どちらのクラスも、HotDrinkをスーパークラスに持っています。つまり、BreakfastにHotDrinkが含まれていて、それがCoffeeなのかTeaなのかわからない場合、デフォルトタイピングを使って解決できます。

public class Breakfast {
   public String food;
   public HotDrink drink;
}

public abstract class HotDrink {
   public String name;
}

public class Coffee extends HotDrink {
   @Override
   public String toString() {
       return String.format("Coffee{name='%s'}", name);
   }
}

public class Tea extends HotDrink {
   @Override
   public String toString() {
       return String.format("Tea{name='%s'}", name);
   }
}

String breakfastJson = """
       {
           "food":"sandwich",
           "drink":["nl.brianvermeer.example.jackson.serialization.Tea",{"name":"oolong"}]
       }
       """;

var om = new ObjectMapper();
om.enableDefaultTyping();

var myBreakfast = om.readValue(breakfastJson, Breakfast.class);
System.out.println("breakfast hotdrink:"+myBreakfast.drink);

この例では、ObjectMapperでデフォルトタイピングを有効にし、このObjectMapperを使うすべての箇所でポリモーフィズムを扱えるようにしました。@JsonTypeInfoアノテーションを使って、特定のフィールドで有効にすることもできます。

Jackson ObjectMapperでデフォルトタイピングを有効にした場合のセキュリティ上の問題

デフォルトタイピングをグローバルに有効にすると、継承の適用範囲が極端に広がる可能性があります。BreakfastにHotDrinkではなくObject型のフィールドがある場合、クラスパス上のあらゆるオブジェクトが使用可能になります。つまり、クラスパス上にある任意のオブジェクトをデシリアライズできるということです。悪用可能なオブジェクトがガジェットチェーンを構成し、最終的にリモートコード実行につながる可能性があります。

こうしたガジェットチェーンは、以前のブログ記事Javaにおけるシリアライズとデシリアライズで紹介したものとよく似ています。初期化時にすぐコマンドを実行する単一のガジェットを使って、簡単な例で説明しましょう。以下は、Object型の飲み物を含むSecondBreakfastクラスです。

public class Gadget {

   private Runnable command;

   public Gadget(String value) {
       this.command = new Command(value);
       this.command.run();
   }
}

public class SecondBreakfast {
   public String food;
   public Object drink;
}

デフォルトタイピングを有効にした状態でSecondBreakfastをデシリアライズすると、任意のコードを実行するObjectをデシリアライズできます。

String secondBreakfastJson = """
       {
           "food":"sandwich",
           "drink":["nl.brianvermeer.example.jackson.serialization.Gadget", "rm -rf *"]
       }
       """;

var om = new ObjectMapper();
om.enableDefaultTyping();

Var mySecondBreakfast = om.readValue(secondBreakfastJson, SecondBreakfast.class);
System.out.println("Second breakfast hotdrink:"+mySecondBreakfast.drink);

これは単純化した例です。ガジェットチェーンを見つけて作成するのは簡単ではありませんが、十分に可能です。さまざまなライブラリやフレームワークを使っているため、クラスパス上のクラスを組み合わせることで、そのようなガジェットチェーンを作成できる可能性があります。

また、危険性が広く知られている「悪意のあるクラス」も数多く特定されています。このようなクラスのデシリアライズは危険と見なされ、基本的には上記のパターンに当てはまります。詳しくは、Snyk Vulnerability Databaseにあるjackson-databindライブラリのデシリアライズ脆弱性の一覧をご覧ください。

アプリケーションにはどのような影響がありますか?

慌てる必要はありません。まず、jackson-databindライブラリのメンテナーは、SubTypeValidatorで「悪意のあるクラス」を積極的にブロックしています。次に、デフォルトタイピングは明示的に有効にする必要があります。つまり、デフォルトではこの設定はオフであり、ポリモーフィックなデシリアライズもできません。

SnykのようなSCAツールでスキャンすると、スキャン結果に脆弱性が表示されます。簡単に調べるだけでも、過去にこうした問題が数多くあったことがわかります。このライブラリは適切に保守されており、新たな「悪意のあるクラス」が見つかれば積極的にブロックされるため、常に最新バージョンに更新するのが賢明です。それでも、JacksonのObjectMapperでデフォルトタイピングを有効にしないことが最善の対策です。

Snykのトリアージアシスタントが解決をサポート

Snykでは現在、コードが前提条件を満たしていない場合に、これらの脆弱性をフィルタリングする取り組みを進めています。Jackson ObjectMapperの場合、コードでポリモーフィックタイピングを有効にしていなければ、該当する脆弱性が悪用される可能性は低いとお知らせします。執筆時点では、この機能はJacksonのデシリアライズ脆弱性にのみ提供されていますが、チームは機能の改善と拡張に取り組んでいます。この機能を利用するには、コードをスキャンしてデフォルトタイピングが有効になっていないことを確認する許可が必要です。

Jackson databindにおける信頼できないデータのデシリアライズに関するSnykの脆弱性詳細。CVE-2019-12384、高い深刻度、修正済みバージョンを表示。

この記事で使用したコード例は、私のGitHubアカウントで公開しています。ご自由にフォークしたり、好きな方法で再利用したりしてください。

安全にデシリアライズしましょう!

Jackson ObjectMapperによるデシリアライズの問題を避けるには、ポリモーフィックタイピングを使わないことが最善です。ObjectMapperでデフォルトタイピングを有効にしないでください。また、プロジェクトをSnykに接続し、既知の脆弱性があるjackson-databindライブラリを使用していないか確認しましょう。ほとんどの場合、新しいバージョンに簡単に置き換えられます。コードスキャンを有効にすると、トリアージアシスタントが脆弱性を悪用される可能性が高いか低いかを判断するのに役立ちます。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。