はじめに
この記事は、記事投稿キャンペーン「【RubyKaigi 2023 連動イベント】みんなで Ruby の知見を共有しよう」の記事です
RubyKaigi 2023 の Day 2 (2023/05/12) 16:00 - 16:30 Takashi Yoneuchi (tw:@lmt_swallow)さんの 「Eliminating ReDoS with Ruby 3.2 」 でお話があった ReDoS のタイムアウトについて実際に動かして検証してみました。
ReDoS について
ReDoS は、Regular expression Denial of Service の略称です
正規表現の評価に時間がかかる文字列を入力しリソースを占有する攻撃です。
ReDoS について詳しくまとめてくださっている @flat-field さんの記事のリンクを張り説明は省略します。
また、 Ruby 3.2 の 正規表現の高速化については、 @WakameSun さんの記事に詳しくまとめられています。
スライドに例として記載されていたコードを実際に動かしてみます
time ruby -e '/^(a|a)*$/ =~ "a" * 10 + "b"'
time ruby -e '/^(a|a)*$/ =~ "a" * 30 + "b"'
Ruby 3.0 の場合
> ruby -v
ruby 3.0.5p211 (2022-11-24 revision ba5cf0f7c5) [x86_64-darwin22]
> time ruby -e '/^(a|a)*$/ =~ "a" * 10 + "b"'
________________________________________________________
Executed in 249.05 millis fish external
usr time 73.83 millis 115.00 micros 73.72 millis
sys time 45.70 millis 656.00 micros 45.04 millis
> time ruby -e '/^(a|a)*$/ =~ "a" * 30 + "b"'
________________________________________________________
Executed in 60.74 secs fish external
usr time 60.37 secs 178.00 micros 60.37 secs
sys time 0.18 secs 911.00 micros 0.18 secs
Ruby 3.2 の場合
> ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-darwin22]
> time ruby -e '/^(a|a)*$/ =~ "a" * 10 + "b"'
________________________________________________________
Executed in 285.32 millis fish external
usr time 70.45 millis 143.00 micros 70.31 millis
sys time 49.34 millis 885.00 micros 48.45 millis
> time ruby -e '/^(a|a)*$/ =~ "a" * 30 + "b"'
________________________________________________________
Executed in 212.96 millis fish external
usr time 66.00 millis 107.00 micros 65.89 millis
sys time 41.94 millis 635.00 micros 41.31 millis
Regexp.timeout=
について
Regexp.timeout=
は Ruby 3.2 から利用出来るようになった global な設定です
Regexp.timeout=
を設定する方法は 2 種類あり、 global に適用指定場合は Regexp.timeout=
のように記述し、 特定の条件の下のみ timeout を Regexp.new(/^(a|a)*$/, timeout: 1.0)
のようにも設定することができます。
# グローバルに定義したい場合
Regexp.timeout = 1.0
# 特定の条件下に閉じ定義したい場合 or グローバル定義を上書きしたい場合
Regexp.new(/^(a|a)*$/, timeout: 1.0)
指定した時間を超え処理を実行しようとした場合 Regexp::TimeoutError
が発生し処理が中断します
> ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-darwin22]
> time ruby -e 'Regexp.timeout = 0.000000001; /^(a|a)*$/ =~ "a" * 30 + "b"'
-e:1:in `<main>': regexp match timeout (Regexp::TimeoutError)
________________________________________________________
Executed in 255.51 millis fish external
usr time 70.34 millis 119.00 micros 70.23 millis
sys time 49.58 millis 688.00 micros 48.89 millis
デフォルトで Regexp.timeout
は未定義となっています。
Regexp::TimeoutError
を発生させたい場合は明示的に指定する必要があります。
また、セッション内でもお話されていましたが、 Regexp.timeout
を定義した場合
Regexp::TimeoutError
が発生しエラーとなるタイミングが同一になります。
詳細は避けますが、攻撃者にとって好都合になる場合もあり、あらゆる可能性も考慮、認識した上で定義することが望ましいと考えられます。
References
- ReDoS から学ぶ,正規表現の脆弱性について - Qiita
- Ruby3.2 の正規表現の高速化を、実際にオートマトンを作って体験してみる - Qiita
- Feature #17837: Add support for Regexp timeouts - Ruby master - Ruby Issue Tracking System
- Introduce Regexp.timeout= by mame · Pull Request #5703 · ruby/ruby
- Eliminating ReDoS with Ruby 3.2 - RubyKaigi 2023
- Make Regexp#match much faster - RubyKaigi 2023
- キャッシュによる Ruby の正規表現のマッチングの高速化の紹介 - クックパッド開発者ブログ
- 第 74 回 正規表現の脆弱性「ReDoS」徹底解説 ~原理と対策から、Perl での最適化まで(1) | gihyo.jp
- Ruby 3.2.0 リリース