LoginSignup
5
2

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

Last updated at Posted at 2023-05-26

はじめに

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

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2