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


## はじめに

この記事は、記事投稿キャンペーン「【RubyKaigi 2023 連動イベント】みんなで Ruby の知見を共有しよう」の記事です

https://qiita.com/official-events/4d2a9eed42dbe0a4af21

RubyKaigi 2023 の Day 2 (2023/05/12) 16:00 - 16:30 Takashi Yoneuchi ([tw:@lmt_swallow](https://twitter.com/lmt_swallow))さんの 「Eliminating ReDoS with Ruby 3.2 」 でお話があった ReDoS のタイムアウトについて実際に動かして検証してみました。

https://rubykaigi.org/2023/presentations/lmt_swallow.html

## ReDoS について

ReDoS は、Regular expression Denial of Service の略称です
正規表現の評価に時間がかかる文字列を入力しリソースを占有する攻撃です。

ReDoS について詳しくまとめてくださっている @flat-field さんの記事のリンクを張り説明は省略します。

https://qiita.com/flat-field/items/f5b0c803ba0b7030d97a

また、 Ruby 3.2 の 正規表現の高速化については、 @WakameSun さんの記事に詳しくまとめられています。

https://qiita.com/WakameSun/items/a54add23f2578a46ebed

スライドに例として記載されていたコードを実際に動かしてみます

```bash
time ruby -e '/^(a|a)*$/ =~ "a" * 10 + "b"'
time ruby -e '/^(a|a)*$/ =~ "a" * 30 + "b"'
```

### Ruby 3.0 の場合

```bash
> 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 の場合

```bash
> 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=` について

https://bugs.ruby-lang.org/issues/17837

https://github.com/ruby/ruby/pull/5703

`Regexp.timeout=` は Ruby 3.2 から利用出来るようになった global な設定です
`Regexp.timeout=` を設定する方法は 2 種類あり、 global に適用指定場合は `Regexp.timeout=` のように記述し、 特定の条件の下のみ timeout を `Regexp.new(/^(a|a)*$/, timeout: 1.0)` のようにも設定することができます。

```ruby
# グローバルに定義したい場合
Regexp.timeout = 1.0
# 特定の条件下に閉じ定義したい場合 or グローバル定義を上書きしたい場合
Regexp.new(/^(a|a)*$/, timeout: 1.0)
```

指定した時間を超え処理を実行しようとした場合 `Regexp::TimeoutError` が発生し処理が中断します

```bash
> 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](https://qiita.com/flat-field/items/f5b0c803ba0b7030d97a)
- [Ruby3.2 の正規表現の高速化を、実際にオートマトンを作って体験してみる - Qiita](https://qiita.com/WakameSun/items/a54add23f2578a46ebed)
- [Feature #17837: Add support for Regexp timeouts - Ruby master - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/17837)
- [Introduce Regexp.timeout= by mame · Pull Request #5703 · ruby/ruby](https://github.com/ruby/ruby/pull/5703)
- [Eliminating ReDoS with Ruby 3.2 - RubyKaigi 2023](https://rubykaigi.org/2023/presentations/lmt_swallow.html)
- [Make Regexp#match much faster - RubyKaigi 2023](https://rubykaigi.org/2023/presentations/makenowjust.html#day1)
- [キャッシュによる Ruby の正規表現のマッチングの高速化の紹介 - クックパッド開発者ブログ](https://techlife.cookpad.com/entry/2022/12/12/162023)
- [第 74 回　正規表現の脆弱性「ReDoS」徹底解説 ～原理と対策から、Perl での最適化まで（1） | gihyo.jp](https://gihyo.jp/dev/serial/01/perl-hackers-hub/007401)
- [Ruby 3.2.0 リリース](https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/)