paild tech blog

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

rustcの脆弱性概説と最新stable追従の重要性

こんにちは、お手伝いの大櫛です。 今回はrustcや標準ライブラリ、Cargoについて発見された脆弱性のいくつかを紹介していきます。

Cargoについての補足ですが、一般的なセットアップツールの一つであるrustupを使う場合、rustcとCargoはRust toolchainとして強く結びついています。このため、今回はrustcと共に紹介したいと思います。

念のための注釈として、この文章中の「Rust x.y.z以降」という表現はx.y.z自身を含むものとします。

また、この記事中ではMIT/Apacheでデュアルライセンスされている標準ライブラリの実装コードを一部引用している箇所があります。

最新stableに追従する重要性

脆弱性の紹介をする前に、rustcの脆弱性対応についてのお話を少ししておきます。

Rustは他の言語にあるようなLTSのような概念を持ちません。重大な問題の修正は最新stableでのみ行われます(厳密にはその時点のbetaやnightlyでも行われますが)。これを踏まえると、脆弱性修正をより早く取り入れる手っ取り早い方法としては最新stableに追従し続けるということになるでしょう。

副次的効果として、脆弱性修正以外にもmiscompilationやICE、パフォーマンスリグレッションなどの開発・運用に多少なりとも影響が出るような問題の修正も素早く取り入れられます。

また、依存しているライブラリが新しいstableでしか使えない機能を使い始めることもあります(いわゆるMinimum Supported Rust Versionに関する問題です)。例えばTokioのようにMSRVについての変更ポリシーを定めているcrateも多いのですが、極論各crateの運用次第ですしやむを得ず変更されることも考えられます。

こういった観点から、最新stableに追従していくことは一定理にかなっている運用だと言えるでしょう。

CVE-2022-46176 (Cargo)

advisory page

こちらは今回紹介する中で最も新しい脆弱性です(執筆時点の最新バージョンは1.68.2です)。

内容としては、CargoがSSHを使って通信する際にhost key(接続先の公開鍵)を検証していなかったというもの。このような通信は依存関係自体やそのindexをcloneする際に発生し得ます。

ssh コマンドを実行する際に "known hosts" に関するメッセージを見かけたことがある人も多いと思いますが、これは接続先とその鍵を覚えておいて以降の通信でそれが変わっていないかを確かめるのに役立ちます。これにより中間者攻撃、雑な言い方だとなりすましに気付けるようになります。

上記の説明をもとにするなら、ここでの脆弱性は「Cargoが "known hosts" 相当の検証を行っていなかった」というものになります。

こちらの脆弱性はRust 1.66.1以降のバージョンで修正されています。

advisoryにも記載されていますが、明示的にSSHを使うよう設定・実行していなくともこの脆弱性の影響を受ける可能性があることに注意してください。advisoryでは「HTTPSからSSHにフォールバックするような設定が適用される場合」が紹介されています。

CVE-2022-21658

advisory page

次は標準ライブラリの脆弱性です。この脆弱性はRust 1.58.1以降で修正されています。

対象となるAPI/functionは std::fs::remove_dir_all で、これは読んで字の如く指定されたパスのディレクトリ自体とその子ファイル・ディレクトリを削除するものになります。

脆弱性の内容としては、攻撃者が任意のディレクトリ(とその中身)に対してシンボリックリンクを作成することで、特定の条件下で削除に必要な権限を持たずとも削除できるようになってしまう、というものです。

実は、これを防ぐような実装は一応なされていました。しかしこの実装は "Time of check to time of use"、いわゆるTOCTTOU(または、TOCTOU)と呼ばれるバグを抱えており、今回の脆弱性に繋がりました。

修正以前の実装は以下の通りです:

pub fn remove_dir_all(path: &Path) -> io::Result<()> {
    let filetype = fs::symlink_metadata(path)?.file_type();
    if filetype.is_symlink() { fs::remove_file(path) } else { remove_dir_all_recursive(path) }
}

処理は単純で、

  1. 与えられたパスのファイルタイプがシンボリックリンクであるか確認
  2. true ならシンボリックリンク自体を削除、 false なら再帰的にそのディレクトリと中身を削除(i.e. std::fs::remove_dir_all の基本動作)

という動作をします。

このとき、シンボリックリンクの確認と削除処理が離れているため、TOCTTOUが存在し得る状態になってしまっているのです。

具体的にどういった操作が必要なのか知りたい、またこの脆弱性を手元で再現したいときはこちらのテストが参考になります。

対応

1.58.0...1.58.1の差分を見る限り、こちらは各OSが提供するAPIを使ってそれぞれに特化した実装を施すことで修正としたようです。

一例としてUNIX系向けの実装ではこちらのような修正がなされています。具体的には openatunlinkat などのAPIが使用されています。openatではファイルディスクリプタを渡せるようになっているので、より厳密にファイルを認識できるようになります(より正確で詳細な説明はopen(2)のmanページを参照するとよさそうです、オンライン版はこちらなど)。

1.58.1では、他にもWindowsやWASI向けの修正が盛り込まれました。

CVE-2021-42574 (Unicode)

advisory page

続いては厳密にはrustcなどの脆弱性ではないのですが、advisoryが用意されたことやパッチリリースもなされていることから、周知の意味も込めて紹介します。

こちらはUnicodeとそのコードポイントについての脆弱性です。こちらはRust 1.56.1以降で修正されています。

Unicodeはアルファベットや平仮名のような左横書きと、ヘブライ文字アラビア文字のような右横書きの両方をサポートしており、これらが混在するテキストも表現可能です。

そのようなテキストをうまく処理するようなアルゴリズムを双方向アルゴリズムと呼ぶらしいのですが、今回の脆弱性はこのアルゴリズムの実装を悪用し、「実際にエディタで表示される文字」と「コンパイル、実行されることになる文字」を異なるものにできる、といったものになります。

例えば以下のような悪意のあるコードがあるとします:

if access_level != "user{U+202E} {U+2066}// Check if admin{U+2069} {U+2066}" {

これが双方向テキストをうまく表示するようなエディタだと、見かけ上は以下のように表示されてしまいます:

if access_level != "user" { // Check if admin

(これらのコードはadvisoryより引用しました)

対応

前述の通りこれはrustc自体に問題があるわけではないので、これらの脆弱性に使われるUnicodeコードポイントの使用を警告するlintを実装することで対応されました。このlintは "deny-by-default"、つまりトリガーされたらコンパイル時エラーになるので1.56.1以降のrustcを使っていれば簡単に発見することができます。

余談ですが、GitHubもこちらの対応を行っており、そのようなコードポイントが含まれる場合Web UI上で警告文が表示されるようになっています。

まとめ

Rustのリリースサイクルは6週間と、プログラミング言語としては周期が短い部類に入るかと思います(すべての言語のリリースサイクルを調べ尽くしたわけではないので雑感に過ぎませんが)。

新機能やバグ修正を比較的すぐに享受できるのは嬉しい一方で、あっという間に最新リリースに置いていかれてしまう懸念も生じます。ユーザー側がうまくrustcのバージョン更新フローを確立させることが肝要です。

幸いなことに、そのようなリリースサイクルの中でRustは後方互換性・安定性に重きをおいて開発されています。そういった意味では気軽に、そして特別な作業なく更新しやすい言語であるという見方もできます。

この記事がそのような更新フローを整えるモチベーションの一欠片になるなど、少しでも役立ちましたら幸いです。