CVE-2023-20861: Spring Expression DoS Vulnerability の影響調査

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

「スタバで注文するのはいつも Venti」でおなじみの fujimura です。

Spring Framework の Security Advisories が出ていたため、ちょっと調べました。

内容

ユーザーが特別に細工した SpEL 式を提供すると、サービス拒否 (DoS) 状態を引き起こす可能性があります。

調査

具体的にどのような修正をすることで対応したのかについては公式ブログから情報を辿ることができました。

v5.2.23 のリリースノートの中の各 issue について読んでみたところ、Improve diagnostics in SpEL で始まる 2 つの commit が該当してるようでした。

コード読解

Improve diagnostics in SpEL for repeated text

こちらは文字列を * 演算子で反復結合した結果の文字列長が 256 よりも長くなった場合に例外を投げるようにすることで、繰り返し回数に大きな数値を与えた時の OOM の発生を抑止するようにしているようです。

Improve diagnostics in SpEL for matches operator

こちらは matches 演算子の右辺の正規表現の文字数が 256 よりも多い場合に例外を投げるようにすることで、複雑な正規表現による計算量の爆発を抑止するようにしているようです。

検証

5.2.23 以前 (脆弱性未対応)

それぞれのパターンで制限を大きく超える文字列を場合は特に制限なく時間を使ってしまうことを確認しました。(具体的には JShell に以下のようなスニペットを渡してみました。)

import org.springframework.expression.spel.standard.SpelExpressionParser;

SpelExpressionParser sep = new SpelExpressionParser();

long startTime1 = System.currentTimeMillis();
sep.parseExpression("'abcd' * 200000000").getValue().toString().length();
System.currentTimeMillis() - startTime1

long startTime2 = System.currentTimeMillis();
sep.parseExpression("'abcd' matches '(" + "0123".repeat(20000000) + ")'").getValue();
System.currentTimeMillis() - startTime2
import org.springframework.expression.spel.standard.SpelExpressionParser

field SpelExpressionParser sep = org.springframework.expression.spel.standard.SpelExpressionParser@626abbd0

field long startTime1 = 1680573406281

sep.parseExpression("'abcd' * 200000000").getValue().toString().length() = 800000000

System.currentTimeMillis() - startTime1 = 2036

field long startTime2 = 1680573408562

sep.parseExpression("'abcd' matches '(" + "0123".repeat(20000000) + ")'").getValue() = false

System.currentTimeMillis() - startTime2 = 1370

5.2.23 (脆弱性対応済)

それぞれのパターンで OK/NG になる SpEL 式を渡してみました。

import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;

SpelExpressionParser sep = new SpelExpressionParser();

Expression exp1ok = sep.parseExpression("'abcd' * 64"); // 256 chars
exp1ok.getValue().toString().length();
Expression exp1ng = sep.parseExpression("'abcd' * 65"); // 260 chars
exp1ng.getValue().toString().length();

Expression exp2ok = sep.parseExpression("'abc' matches '(" + "0123456789".repeat(25) + ")'"); // 253 chars
exp2ok.getValue();
Expression exp2ng = sep.parseExpression("'abc' matches '(" + "0123456789".repeat(26) + ")'"); // 263 chars
exp2ng.getValue();

実行結果は以下のようになりました。

Defined import org.springframework.expression.Expression
Defined import org.springframework.expression.spel.standard.SpelExpressionParser

Defined field SpelExpressionParser sep = org.springframework.expression.spel.standard.SpelExpressionParser@157853da

Defined field Expression exp1ok = org.springframework.expression.spel.standard.SpelExpression@2accdbb5

exp1ok.getValue().toString().length() = 256

Defined field Expression exp1ng = org.springframework.expression.spel.standard.SpelExpression@6a84a97d

EL1076E: Repeated text results in too many characters, exceeding the threshold of '256'
exp1ng.getValue().toString().length()

Defined field Expression exp2ok = org.springframework.expression.spel.standard.SpelExpression@21282ed8

exp2ok.getValue() = false

Defined field Expression exp2ng = org.springframework.expression.spel.standard.SpelExpression@6c4906d3

EL1077E: Regular expression contains too many characters, exceeding the threshold of '256'
exp2ng.getValue()

NG のパターンでそれぞれ閾値 (256) を超えた旨をエラーメッセージが表示されていることが確認できます。

まとめ

  • 対応が入っているバージョンにアップデートをする。
  • 何らかの理由で修正が入っているバージョンにアップデートできない場合は外部から HTTP リクエストなどで渡された (= 第三者が任意に指定できる) 文字列を SpEL 式として解釈するような処理がないか確認する。
    • そもそも第三者が任意に指定できる文字列をそのまま命令列として解釈するような処理がある時点でセキュリティ的には十分に危険ではあるため、仕様レベルでの見直しを検討した方が良いでしょう。