CVE-2023-20861: Spring Expression DoS Vulnerability の影響調査
「スタバで注文するのはいつも 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 式として解釈するような処理がないか確認する。
- そもそも第三者が任意に指定できる文字列をそのまま命令列として解釈するような処理がある時点でセキュリティ的には十分に危険ではあるため、仕様レベルでの見直しを検討した方が良いでしょう。