paild tech blog

法人カードのクラウド型発行・管理サービスpaild

Rust 外部crates脆弱性概説と便利ツール

こんにちは、お手伝いの大櫛です。 今回は、rustc/Cargoまわりの脆弱性紹介に引き続き、community crates関連の脆弱性を紹介していきます。

rustcと違い、ある脆弱性一つがRustユーザー全体に影響を与えるものではないため、比較的広くつかわれているcrateのものや、個人的に面白いと思ったものを取り上げます。必ずしも今回の脆弱性すべてがCVSSスコアで言うところのクリティカルなものではないこと、今回紹介した以外にもクリティカルな脆弱性は存在し得ることをご留意ください。

また、筆者の理解不足により、脆弱性の説明において誤った内容を述べている可能性があります。そのような記述を見つけた際には何かしらの形でお知らせいただけますと幸いです。

対象とする脆弱性について

RustSec Advisory DatabaseまたはGitHub Advisory Databaseに登録されている脆弱性を対象とします。前者のadvisoryはSecure Code working groupというRust公式のワーキンググループによって管理されています。

rustsec.org

RustSec Advisory Databaseでは、脆弱性自体以外にも、その予備軍としてunmaintainedなcrate群も登録されています。

後者のGitHub Advisory DatabaseでもRustに関する脆弱性を確認できます。こちらはGitHub repositoryのSecurityタブから確認できるなど、GitHubとの親和性が高いつくりになっています。

Rustの依存関係に含まれる脆弱性を検知する仕組みについて

Rustの脆弱性検知には、cargo-auditと呼ばれるbinary crate(cargo subcommand)が有用です。

こちらもRustSecによって管理されており、インストール後に cargo audit を実行することで、依存関係に持っているcrate(とversionのペア)の中でadvisoryに登録されている脆弱性がないか検査できます。

また、ボーナスポイントとしてcargo-denyも紹介しておきます。

こちらはcargo-auditの機能に加え、依存関係の中に許可していないライセンスや信頼できないソース(Git repositoryなど)が含まれていないかなどについても検査できます。ゲームスタジオのEmbark Studios謹製で「自社で使っているツールをオープンソース化した」とのことで、ある程度の実用性を期待できます。Rustのビジネスユースを考える際に特に有用なツールだと言えます。

これらをGitHub Actionsでうまく使うためのActionも提供されていますので、興味がある方はぜひ見てみるとよさそうです。

RUSTSEC-2022-0013

ここからは本題の脆弱性紹介です。

まずはじめに正規表現を扱うregexでの脆弱性です。advisoryはこちらで、修正パッチはこちらです。

regex crateはRustで正規表現を扱うなら必ず通るほどのcrateで、特にRIIRの代表的なツールであるripgrepでも使われています。

今回の脆弱性は、以下のような正規表現コンパイルしようとすると膨大な時間がかかってしまう、というものになります:

(?:){294967295}

(?:) はnon-capture groupと呼ばれるもので、例えば (?:x) とすると「captureはしないが x にmatchする」という表現になります。上記例の場合何も指定されていないので空(empty)にmatchします。

{n} は前にあるグループなどをn回繰り返す表現で、例えば (?:x){10} とすると「 x が10回繰り返されたらmatchする」という表現になります。上記例の場合294967295回繰り返します。

以上をまとめると「空を294967295回繰り返したものにmatchする」表現となり、つまり自分の知る限り実用性のない正規表現となるわけですが、この繰り返し分regex compilerが処理を行ってしまうというのが今回の脆弱性です。

regex crateでは信頼できない入力、例えばユーザーから正規表現を受け取って処理をするようなプログラムのために、このようなnが大きすぎる表現のコンパイルを防ぐ機構を持っていたのですが、今回鍵となったのは (?:) の部分です。

この {n} のnが大きすぎることをregex instructionのメモリ量で検知するよう実装されていたのですが、(?:) ではinstruction countが増加しないためバイパスできる状態になってしまっていたのです。

修正パッチではこのemptyなケースについて、仮想的にメモリ量を増やして管理・大きすぎた場合にはエラーを返すように実装されました。

github.com

これはemptyな表現を最適化していたからこその脆弱性で、そこをつくような正規表現ケースを指摘できるのは面白いなと感じました。

RUSTSEC-2023-0034

次にh2という、HTTP/2のクライアント・サーバー実装を提供するライブラリで発見された脆弱性について紹介します。advisoryはこちらで、修正パッチはこちらです。

概要としては、「streamの開始と終了を行うframeをh2が処理できない速度で送り続けるとメモリ使用量が増加してしまう」というものになります。攻撃者がそのようなリクエストを大量に送信できる場合、この脆弱性を利用できます。

具体的には、streamを終了するための RST_STREAM というframeを接続先から受け取ったときに行う処理が完了する前にもう一度 RST_STREAM を送信すると、先行している処理が完了するまで後続の処理はqueueに入れられ保留されます。要はこのqueueを肥大化させてメモリ使用量を悪意を持って増やせるというのが今回の脆弱性です。

今回は件のqueueに上限値を設けることで修正としたようです。

github.com

以上が簡単な概要なのですが、この脆弱性については少しよくない流れで修正された経緯があります。

元々この問題はhyperというHTTPクライアントのissue trackerで起票されていました(h2とhyperは密接な関係にあり、hyperがh2を依存関係として持っています)。このissueではメンテナも返答はしていたものの、1年近くopen(not addressed)のままになっていました。

github.com

そんな中あるユーザーがCVE IDのアサインをリクエストしそれが通ったことで流れが急変します。これをきっかけとしてGitHub Advisory Databaseにも登録された結果、h2やhyperを依存関係に持つrepository各所でsecurity alartが発火されるようになってしまったのです。

当然この時まだ修正のリリースはおろか実装PRすら提出されていません。これについてメンテナは「このような形でCVE IDをアサインするのはライターを見た瞬間に火災警報器を鳴らすようなものだし、まずはセキュリティポリシーを参照してほしい」という旨の苦言を呈しています

実はこのような、脆弱性(厳密にはメンテナらによってレビューされていないので脆弱性候補と呼ぶのが適切かもしれません)がpublicに報告されメンテナに相談なくCVE IDがアサインされるという流れは今回に限った話ではありません。自分の知るところだと、コンテナ向けツールであるruncでも似たような騒動がありました

github.com

昨今はOSS脆弱性について意識が向上している空気を感じているのですが、一方で今回のような流れはそれ自体がゼロデイ攻撃に繋がりかねません。知らず知らずのうちにbad actorにならないためにも、報告窓口の把握を始めとする取り決めはしっかり確認・遵守しておきたいところです。

RUSTSEC-2022-0090, RUSTSEC-2022-0051

また、Rust以外の言語の実装で見つかった脆弱性がRust実装に影響する場合もあります。

RUSTSEC-2022-0090RUSTSEC-2022-0051がその好例です。

これらのcrateはCでの実装をバンドルしているいわゆる *-sys crateなのですが、他言語実装を"バンドル"しているが故にそこで脆弱性が発見されると直に影響を受けます。

全ユーザーが直接使うものではないと思いますが、他方libsqlite3-sysのように低レベルでのコミュニケーションを必要とする場合には推移的依存関係として含まれている可能性が高いでしょう。もしそのようなsys crateが依存関係にある場合はバンドル先の脆弱性情報にも目を光らせる必要がありそうです。

幸い、RustSec Advisory Databaseにはそのような脆弱性も登録されるようになっていますので、cargo-auditなどで通知するようにしておけば一定安心です。

GHSA-c25x-cm9x-qqgx

最後にRustユーザーの直接的な依存関係にあるものではないのですが(なのでRustSecのadvisoryとしては登録されていません)、関連情報としてRustで実装が行われているDenoの脆弱性を紹介します。advisoryはこちらです。

Deno 1.32.0にて"Resizable and Growable ArrayBuffers"のサポートが追加されたのですが、この実装に脆弱性があったようです。この機能はTC39のStage 3 proposal(記事執筆時点)をもとにしています。

github.com

ここで提案されている機能はメモリ管理をより賢く行うため、(Shared)ArrayBuffer インスタンスの最大サイズを予め設定できたりそれに応じてresize/growをコスト低く行えたりするものになります。詳しい内容はぜひプロポーザルをお読みください。

問題はこのresize処理を非同期に行った際、mutabilityがうまく考慮されておらず読み書き処理においてout-of-bound状態が発生してしまう、というものです。

これは機能自体に問題があったのではなく、非同期I/OにおけるDenoでのArrayBufferの扱いをうまくデザインしきれていなかったことに起因するようです。詳細が以下のissueで説明されています(このissue自体は件のプロポーザルがDenoで実装、そして脆弱性が発見される前にopenされています)。

github.com

descriptionを読む限り、いわゆる"shared XOR mutable"ルールが担保されていないことが原因のようです。このあたりでSAFETYコメントとして操作の安全性が説明されていますが、非同期I/Oにおいてはコメントでの前提と実際の動作に乖離があったように見受けられます。

これを受けてDeno 1.32.1ではresizable ArrayBuffer機能が無効化されました。記事執筆時点では正式な修正パッチは開発中であり、どのように解決されるのか注目です(ドラフトとして以下PRが提出されています)。

github.com

Denoのように、Rust実装から他言語実装とコミュニケーションするようなプログラムは今後も出てくることが予想されます。そういう意味では直接的なRustユーザーではなくとも、Rust関連の脆弱性についてキャッチアップをすることに一定の価値があるように感じます。

まとめ

今回紹介したものはRust全体で見てもほんの一部に過ぎません。RustSec Advisory Databaseを眺めていただければお分かりいただけると思うのですが、unmaintained含め多くのadvisoryが登録されています。

rustcと違い、外部crateについての依存関係のバージョンや脆弱性把握は容易なことではありません。たとえ直接的なものが少なくても、推移的依存関係を含めた数は膨大なものになり得ます。その中でDependabotやRenovateなどによる依存関係の更新補助、あるいはさきに紹介したcargo-auditやcargo-denyによる検知など、リスク軽減・防止につながるいろいろなツールが提供されています。

メインの開発を極端に妨げないようそのようなツールを使って運用コストを減らしつつ、脆弱性のようなリスクと付き合っていくのがひとつバランスの取れた運用なのかなと感じています。

ただし、上記だけですとあくまでRustSec Advisory Databaseにある脆弱性しか対象になりません。登録までに時間がかかっていたり、あるいはそもそも登録作業が行われていなかったりするケースがあることにも注意すべきでしょう。