Circeで重複したキーがしたJSONのパース時にエラーを発生させる

circeでキーの重複を許容しない場合はparserパッケージのオブジェクトではなく独自に生成したJawnParserを使用します。
2021.10.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

ScalaのJSONパーサー circeがデフォルトではキーが重複しているJSONオブジェクトが許容される挙動だったのでその実装を調べて対策をしました。

なぜキーが重複するとダメなのか?

以下のようなバリデーション対象のフィールドfields_to_validateを含むJSONをパラメータとして受け取るシステムがあったとします。

このシステムは受け取ったJSONをバリデーションしてバックエンドの別システムに渡します。バックエンドシステムはバリデーション済みのJSONを受け取ることを前提としていて、バリデーションや入力値からXSSやSQL Injection対策のためのエスケープを行いません。

{
    "fields_to_validate": "1234"
}

ここで、以下のように重複したキーを持つJSONが渡される場合を考えます。

{
    "fields_to_validate": "1234",
    "fields_to_validate": "alert('hi')"
}

もし2つのシステムのJSONのパーサーの解釈が異なる以下のようにJSONが解釈された場合、上記のJSONの2つ目の値がバックエンドシステムで使用されてしまい、XSSなどの攻撃が成立する可能性があります。

  • フロントシステム: fields_to_validate = 1234 はvalid (数字だけなのでヨシっ)
  • バックエンドシステム: fields_to_validate は valid (フロントエンドがバリデーションしているのでヨシッ)

circeでの対策

CirceのJSONパーサーの実装(parserパッケージのparser)であるJawnParserはコンストラクタのパラメータallowDuplicateKeysでキーの重複の許可を設定できます。

@ import $ivy.`io.circe:circe-parser_2.13:0.14.1`

@ io.circe.parser.parse(s"""{"k": 1, "k": 2}""")
res10: Either[io.circe.ParsingFailure, io.circe.Json] = Right(
  JObject(object[k -> 2])
)

@ new io.circe.jawn.JawnParser(None, false).parse(s"""{"k": 1, "k": 2}""")
res11: Either[io.circe.ParsingFailure, io.circe.Json] = Left(
  ParsingFailure(
    "Invalid json, duplicate key name found: k",
    java.lang.IllegalArgumentException: Invalid json, duplicate key name found: k
  )
)

まとめ

circeでキーの重複を許容しない場合はparserパッケージのオブジェクトではなく独自に生成したJawnParserを使用します。