RubyRubyKaigiReDoSRuby_記事投稿キャンペーンRubyKaigi2023

Ruby 3.2 で ReDoS 対策/改善のために追加された `Regexp.timeout=` について

はじめに

この記事は、記事投稿キャンペーン「【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