じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

PHP2Shellから考える、Rustは何を防ぎ、何を防がないのか

はじめに

PHP2Shellへの対応、お疲れさまでした。この記事は、更新や影響の確認が一段落し、今後の開発や保守を考える人に向けた読み物です。

2026年10月21日、PHPは外部からのリクエストによって、サーバー上で任意のコードを実行されうる2件の脆弱性を修正しました。PHP2Shellと呼ばれる問題です。PHP公式はWeb向けの各実行方式に影響し、Apacheのmod_phpではアプリケーションのコードが動く前から攻撃が成立すると説明しています。

www.php.net

GMO Flatt Securityの解説がめちゃくちゃ良いので、何が起こったのかは、まずこちらを読んでみてください。

blog.flatt.tech

開発者が気をつけて防いできた不具合のなかには、言語を替えることで防げるものがあります。ただ、言語が保証する安全性と、ソフトウェアを安全に使い続けられることは、同じではありません。Rustへの移行を考えるときにも、何を言語に任せられ、何を設計や運用で確かめ続ける必要があるのかを知りたいです。

PHPを貶して「Rust最高」で終わる話にはしたくありません。長く使われてきた技術には、その時代の制約のもとで積み重ねられた判断があります。既存の動作を保ちながら改良を続けてきた人たちの仕事も含めて、今のソフトウェアは成り立っています。

その歴史と保守を続けてきた人たちに敬意を払いながら、今の条件では何を変えるとよいのかを考えたいです。PHPを更新しながら使う方法も、必要な部分をRustへ移す方法もあります。ノリでRustを入れると、今度は運用の負担が増えます。開発速度も落ちるかもしれません。

まず知りたいのは、どの人たちが、どの仕事で困っているのかです。私はその人たちの困りごとと、導入後の仕事の変化を説明できない技術選定には価値がないと考えています。開発や運用を担うメンバーと一緒に、何を改善し、誰が新しい作業を引き受けるのかを決めてから、Rustが合うかを確かめたいです。

PHP2Shellで何が起きたのか

私は普段、バックエンドやインフラの開発・運用をしていて、セキュリティの仕事にも関わっています。ただ、脆弱性の発見や攻撃手法の研究を専門にしているわけではありません。以下は、公式のアドバイザリや修正差分、研究者の報告をもとに、開発・運用の立場から理解した内容です。

今回の問題があるのは、PHPで書かれたアプリケーションを実行する「PHP処理系」の内部です。処理系は主にCで実装されています。利用者のリクエストを受けると、入力を解析し、データをメモリに置き、アプリケーションへ渡します。処理が終わったら、不要なメモリを解放します。PHP2Shellはこの準備や後片付けの不具合を利用する攻撃です。

メモリ不足になったときは、エラーを返すかプロセスを終了させ、有効でないメモリを使わずに止まる必要があります。ところがPHP2Shellでは、中断した処理の後片付けや、次のリクエストの処理が、そのメモリを使ってしまいました。

後片付けが未初期化のデータを使う

CVE-2026-109618では、メモリ不足への対処が問題になります。メモリ不足はOOM、Out Of Memoryとも呼ばれます。PHPはOOMになると、longjmp()という仕組みで、実行中の処理から離れたエラー処理へ移動していました。途中にある処理がすべて最後まで実行されるわけではありません。

内部の配列やオブジェクトを作る途中で中断すると、管理情報には「要素がある」と記録されていても、要素の中身はまだ書き終わっていないことがあります。後片付けがその管理情報を信じると、未初期化の中身を有効なデータとして読み取ってしまいます。攻撃者は入力の解析によってメモリ消費が増えるようなリクエストを使い、この中断を発生させます。通信上のサイズが小さくても、PHPの内部表現へ展開した後のメモリ消費は大きくなりえます。

github.com

PHP 8.4.26のZend/zend_hash.cにある_zend_hash_str_add_or_update_i()は、配列へ文字列のキーと値を追加する関数です。次の抜粋では、途中のデバッグ用処理だけを省略しています。

idx = ht->nNumUsed++;
ht->nNumOfElements++;
p = ht->arData + idx;
p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
/* デバッグ用の処理を省略 */
p->h = ZSTR_H(key) = h;
HT_FLAGS(ht) &= ~HASH_FLAG_STATIC_KEYS;
if (flag & HASH_LOOKUP) {
    ZVAL_NULL(&p->val);
} else {
    ZVAL_COPY_VALUE(&p->val, pData);
}

github.com

htは配列の管理情報を指すポインタです。ポインタはメモリ上のデータの場所を保持する値です。Cのht->nNumUsedという書き方は、htの指す管理情報にあるnNumUsedという項目を表します。この配列はキーと値をまとめたBucketという要素を並べて保持しています。

最初の2行で、使用した領域の長さを表すnNumUsedと、要素数のnNumOfElementsを増やしています。続くp = ht->arData + idxで追加先の要素を指し、4行目のzend_string_init()でキーを保存するメモリを確保します。値の初期化はその後のZVAL_NULL()やZVAL_COPY_VALUE()です。

要素が未完成なのに、後片付けの対象として数えてしまっています。 キーの確保中にOOMで処理を抜けると、その関数の戻り値をp->keyへ代入する処理も、下にある値の初期化も完了しません。それでも、すでに増やしたnNumUsedは戻りません。

同じファイルのzend_array_destroy()は、次の2行で後片付けの対象範囲を決めます。

Bucket *p = ht->arData;
Bucket *end = p + ht->nNumUsed;

github.com

この後、endまでの要素を順に読み、値やキーを解放します。増やした範囲には、まだ正しい値を書いていない要素も含まれます。後片付けはそのメモリを有効な値として扱い、値の種類に応じた処理を選んでしまいます。メモリ不足で止まるはずが、未初期化のメモリを読み、さらに誤った解放処理へ進んでしまうのです。

ApacheへPHPを組み込むmod_phpでは、リクエストの準備に失敗しても後片付けが走ります。そのため、入力を解析した段階のOOMから、アプリケーションが動く前に攻撃が成立します。ログイン処理やアプリケーション内の入力チェックまで到達していないので、そこへ対策を足しても間に合いません。

次のリクエストが解放済みメモリを使う

CVE-2026-109619では、ワーカーに残った古いポインタが問題になります。ワーカーはリクエストを処理するプロセスなどを指します。1つのリクエストを終えた後も、終了せずに次を受け付けます。

リクエスト専用のメモリを解放したのに、ワーカー側はその場所を示すポインタを保持し続けることがあります。次のリクエストが通常の初期化より先にそのポインタの参照先を使うと、解放済みのメモリを使ってしまいます。これがUse After Free、解放後の使用です。原因はOOMに限りません。エラーでリセットを飛ばす場合や、終了処理中の警告で状態を再び書き込む場合もあります。攻撃の成立には、古い状態を残す操作と、それを同じワーカーで後から使う操作の両方が必要です。

github.com

PHP 8.4.26のext/standard/var_unserializer.reには、逆シリアル化で使う次のコードがあります。逆シリアル化とは保存用の表現からPHPの値へ戻す処理です。.reはCのパーサーを生成するための元ファイルで、以下はその中のCコードです。

BG(unserialize).data = d;
BG(unserialize).level = 1;

dは、逆シリアル化の参照追跡や後始末に使う管理データへのポインタです。この時点では初期化が済んでいます。BG(unserialize)はワーカーに残る管理領域を指す書き方で、dataにポインタを保存し、levelに処理の入れ子の深さを記録します。ここで保存しているのは、セッションの内容そのものではありません。

同じ処理へ入れ子で入ったときは、別の分岐で既存の管理データを共有します。

d = BG(unserialize).data;
++BG(unserialize).level;

github.com

正常に終われば、対応する後始末で管理データを解放し、最外層を抜けたところでdataをNULLへ戻します。NULLは、参照するデータがないことを表す値です。ところが、途中の致命的なエラーで後始末へ到達できないと、リクエスト用メモリだけが解放され、dataとlevelはワーカー側に残りえます。

次のリクエストで通常の初期化が済めば、古い状態はリセットされます。しかし、アップロードの進捗を調べる処理は、それより前にセッションを読み出せます。その際に古いlevelを見て「処理が入れ子になっている」と判断すると、上の分岐で古いdataを再利用してしまいます。

ポインタをコピーしただけで、直ちにその先のメモリを読むわけではありません。実際に参照先を読む箇所の1つが、同じファイルの次の行です。

var_hash = (*var_hashx)->last_dtor;

github.com

(*var_hashx)で管理データへのポインタを取り出し、->last_dtorでその中の情報を読みます。そのポインタが前のリクエストの解放済みメモリを指していれば、Use After Freeになります。この経路には、古い状態を残す中断と、同じワーカーで通常のリセットより先に読み出す処理の両方が必要です。CVE-2026-109619にはほかの経路もありますが、この例では「初期化が未完了」ではなく「有効だったデータの寿命が終わった」ことが問題です。

メモリを解放しても、その場所が永久に使用禁止になるわけではありません。別のデータのために再利用されます。攻撃者がメモリの配置や内容を調整できると、PHPが別のデータを内部オブジェクトなどとして誤って扱う状況を作れます。研究者はこうしたメモリ破壊を任意コード実行までつなげたと報告しています。

calif.io

任意コード実行、RCEとは、攻撃者が選んだ処理をサーバー側で実行させることです。そのプロセスの権限で読めるデータや実行できる操作が被害の対象になります。メモリ不足を起こすだけで、必ずRCEになるわけではありません。処理が止まったことを確認しても、メモリ破壊やRCEを確認したことにはなりません。

PHPのワーカーをWebサーバーとは別のプロセスとして動かすPHP-FPMでは、CVEごとに攻撃の条件が異なります。CVE-2026-109618のアドバイザリによると、FPMはリクエストの準備に失敗すると子プロセスを終了します。そのため、mod_phpで確認された経路がそのまま成立するとはしていません。FPMも影響を受けうるとしつつ、このCVEについて完全なFPM向け攻撃の実証は記載していません。一方、CVE-2026-109619では、アプリケーション実行中の処理を利用するFPMでのRCEが実証済みです。

OOMの処理とリクエスト開始時のリセットを修正する

PHPの修正では、OOM発生直後に処理を飛び越える方式を改めています。通常の上限に達すると、PHPの実行を担うVMが状態を安全に扱える地点でエラーを処理します。さらに高い上限を超えると、_exit()でプロセスを終了させます。リクエスト間で持ち越してはいけない状態のリセットも追加されています。

github.com

BG(unserialize)の例では、入力を扱う前にもリセットする処理が追加されました。PHP 8.4.27のmain/main.cでは、起動処理の早い段階で次の順に呼び出します。

zend_activate();
php_basic_early_rinit();
sapi_activate();

github.com

追加されたphp_basic_early_rinit()には、次の行があります。

memset(&BG(unserialize), 0, sizeof(BG(unserialize)));

github.com

memset()は、指定した範囲を同じ値で埋めるCの関数です。この実装では管理領域をゼロにし、古いポインタと入れ子の深さを消します。通常の初期化にもリセットは残っているため、「リセットを移した」というより、早く動く入力処理の前にも追加した修正です。

一方の修正はOOMを扱うタイミングを変え、もう一方は古いポインタを使う前に消します。どちらも入力の特定文字列を拒否するだけでは取り除けない不具合です。データを作る順序、解放するタイミング、リセットを呼ぶ順序を処理系内部で改める必要がありました。

共通するのは、正常に最後まで動けば整合する処理が、中断や再利用を挟むと成立しなくなることです。値を作る処理だけでなく、途中で失敗した後に何を読んでよいかまで守る必要があります。言語を替える効果を考えるときも、この条件を言語の仕組みに任せられるかが重要になります。

PoCが実証した範囲を読む

PoCは、特定の条件で不具合や攻撃が成立することを実証するためのコードなどを指します。

Califは複数のPHPアプリケーションに対する認証前RCEを報告し、mod_phpとPHP-FPMのデモを公開しています。WizもWordPress、Drupal、JoomlaでのRCEと、PHPプロセス内のメモリからの情報漏えいを報告しています。いずれも研究者による実証報告です。

www.wiz.io

同じCVE番号を掲げたGitHubリポジトリでも、研究者の実証と同じ攻撃を再現できるとは限りません。次の3ファイルは公開ソースを読んで動作と前提条件を確認したものです。これらを使って手元でRCEを再現したわけではありません。

確認した公開コード コードが行っていること そこから判断できる範囲
ikow/php2shell-pocのChain A セッションファイルへ内容を書き込み、別途存在するLFIを利用する アプリケーション側のファイル読み込みの欠陥が前提。今回のメモリ破壊そのものを証明するコードではない
同リポジトリのOOM確認用コード メモリ消費を狙うリクエストを送り、HTTP 500や応答断を観察する 応答だけでは、OOM、メモリ破壊、RCEを区別できない
同リポジトリのFPM確認用コード 連続するリクエストの応答を調べ、502などを脆弱性ありと判定する 502だけではUse After Freeの発生箇所やRCEの成立までは証明できない

確認したコミットは5baea61です。LFIはLocal File Inclusionの略で、外部入力に影響されるパスのファイルをアプリケーションが取り込んでしまう問題です。このChain Aでは、そのファイルの内容をPHPとして実行することが前提になっています。

github.com

github.com

github.com

PoCの評価では、前提条件と対象側の記録を照合します。クラッシュならプロセスの終了、RCEなら意図した処理の実行を示す証拠が必要です。逆に、あるPoCが失敗しても、別の環境や経路で成立する可能性は残ります。手元で再現できなくても、公式の情報で影響が確認できれば、更新を進めます。

Rustはメモリの寿命と使い方を制限する

Rustは所有権、借用、型、実行時の検査を組み合わせ、無効なメモリへアクセスする操作を制限します。この保証には、コンパイラや標準ライブラリ、依存先のunsafe実装などが正しく動作するという前提があります。

防ぎたい失敗 Rustが使う仕組み
解放済みメモリの使用、同じ資源の二重解放 所有権で破棄を管理し、借用した参照が指すデータより長く使われないよう検査する
未初期化の値を通常の値として使用する 初期化の確認と型の規則を使う。値がまだない状態はOption<T>などで表す
配列の範囲外を読み書きする 通常のインデックスアクセスで境界を検査する。範囲外ならpanicし、get()ならNoneを返す
同じメモリへの同期されない読み書き 借用の規則、SendとSync、同期プリミティブを組み合わせてデータ競合を防ぐ

参照と所有権の関係はThe Rust Programming Language、境界検査はVecの公式ドキュメントに説明されています。

doc.rust-lang.org

doc.rust-lang.org

PHP2Shellでは、前のリクエストで解放したメモリを次のリクエストが使っていました。Rustでリクエストが所有する値への参照を外へ持ち出しても、所有者を破棄した後でその参照を使うコードは、通常の借用検査で拒否されます。長く保持したければ、値の所有権を移すか、Arc<T>などで所有を共有し、必要な間は値を生存させます。

たとえば、次のコードです。Stringは文字列を所有する型で、&requestはその値への参照を作ります。参照を作るだけでは、文字列の寿命は延びません。

let response;
{
    let request = String::from("request-owned data");
    response = &request;
}
println!("{response}");

内側のブロックを抜けると、requestが持つ文字列は破棄されます。その後でresponseを使おうとしているので、このコードはコンパイルできません。コンパイラが参照と値の寿命の不一致を検出し、実行前に拒否します。値を外へ持ち出すなら、所有権を移せます。

let response = {
    let request = String::from("request-owned data");
    request
};
assert_eq!(response, "request-owned data");

今度は文字列の所有権がresponseへ移ります。内側のブロックを抜けても、文字列は外側で使えます。どちらもdoctestという、説明文中のコードを検査するテストに入れました。前者がコンパイルできず、後者が動くことを確かめています。

配列の添字などはコンパイル時にすべて確認できるわけではありません。実行時に範囲外へのアクセスを見つけて処理を止める場合があります。メモリの不正な読み書きは防げても、リクエストに応答できるとは限らないのです。

Rustのunsafeでも未初期化のメモリを使ってしまう

Rustの可変長配列Vec<T>も、要素を保存するメモリと、要素数を別々に管理します。2個分の容量を確保しても、値を書き込んでいなければ要素数は0個です。通常のAPIでは、要素の初期化と要素数の更新をVecの実装側が管理します。一方、unsafeなset_len()を使うと、呼び出す側が要素数を直接変更できます。安全に使うには、増やす範囲の初期化を終え、確保済みの容量を超えないことを呼び出す側で保証しなければなりません。

doc.rust-lang.org

unsafeを使う実装について解説したRustonomiconには、要素数を先に増やしてから値を複製する、誤った実装例が載っています。途中のclone()がpanicすると、まだ値を書いていない領域まで「要素がある」と記録されたままになります。その領域を読んだり、破棄したりすると、メモリを不正に使ってしまいます。

doc.rust-lang.org

次の2つのコードは、要素を2個入れる途中でpanicを起こし、中断後のVecを調べます。panicは、Rustが処理をそのまま続けられないときに使う仕組みです。この検証では既定の巻き戻しを使い、catch_unwind()でpanicを捕まえます。PHPのOOMやlongjmp()の再現ではなく、初期化の途中で処理が中断したときの検証です。

まず、safeなVecのAPIだけを使うテストです。inspect()の処理は、各要素をVecへ渡す前に走ります。

use std::panic::{AssertUnwindSafe, catch_unwind};

#[test]
fn safe_vec_keeps_only_initialized_elements_after_panic() {
    let mut values = Vec::with_capacity(2);
    let interrupted = catch_unwind(AssertUnwindSafe(|| {
        values.extend([7_u8, 8].into_iter().inspect(|&value| {
            assert_ne!(value, 8, "injected interruption before the second element");
        }));
    }));

    assert!(interrupted.is_err());
    assert_eq!(values, [7]);
    assert_eq!(values.get(1), None);

    // 中断しても、初期化済みの要素だけを持つVecとして使える。
    values.push(9);
    assert_eq!(values, [7, 9]);
}

2個目を渡す前にpanicさせても、Vecは「1個の要素を持つ」という正しい状態を保ちます。ただし、最初に追加した7は残ります。メモリ安全であることは、操作全体が取り消されることまで意味しません。AssertUnwindSafeも、巻き戻しで業務上の整合性が回復することを検証する機能ではありません。このテストでは、中断後にどの値が残るかまで確認しています。

次は意図的に誤ったunsafeの使用です。検証用のプログラムから、Miriで動かす分岐を示します。Miriはコードを実行しながら、不正なメモリの使用などを検出するRustの検査ツールです。

#[cfg(miri)]
fn main() {
    use std::panic::{AssertUnwindSafe, catch_unwind};

    let mut values = Vec::<u8>::with_capacity(2);
    let interrupted = catch_unwind(AssertUnwindSafe(|| {
        // 誤り:2個の要素が初期化済みだと、先に宣言してしまう。
        unsafe {
            values.set_len(2);
        }
        values[0] = 7;
        panic!("injected interruption before initializing the second element");
    }));
    assert!(interrupted.is_err());

    std::hint::black_box(values[1]);
}

容量が2個分あることと、2個の値を書き終えたことは違います。set_len(2)を呼ぶ側は、2個とも初期化済みであることを保証する必要があります。このコードはその条件を破っています。panicを捕まえた後に2個目を読むと、Miriは次のエラーを報告し、終了コード1で停止しました。診断の一部を抜粋します。

error: Undefined Behavior: reading memory ... but memory is uninitialized

未定義動作とは、言語がプログラムの動作を保証しなくなる操作です。Miriの検出箇所が読み取りであることは、それより前のset_len()の使い方が正しかったという意味ではありません。サンプルには通常実行時のガードも置き、Miriを使わなければこの処理を実行しないようにしています。

PHPの配列の例でも、このVecの例でも、値を書く前に要素数を増やしています。途中で失敗すると、後の処理が未初期化のメモリを読んでしまいます。Rustでも、unsafeを使って配列を実装する側は、中断後に残る値と要素数が一致するように作る必要があります。safeなVecのAPIを使う側は、この初期化順序を呼び出すたびに確認する必要がありません。

Rustを使う利点は、こうした確認をライブラリの実装にまとめ、その外側で誤った使い方を制限できることです。safeなAPIを提供するなら、呼び出す側がsafe Rustでできる操作によって、内部のメモリ安全性が壊れてはいけません。今回のように、渡された処理が途中でpanicすることも含みます。unsafeの行数だけを減らすのではなく、どの前提を実装側が守り、呼び出す側に何を許すかを設計します。

unsafeは、コンパイラによる検査をすべて無効にする機能ではありません。生ポインタの参照先を使うなど、コンパイラだけでは安全性を確認できない操作を許します。実装者は参照先のメモリがまだ有効かを確かめます。その型に合う値で初期化されていることも確認します。

doc.rust-lang.org

自分のコードでunsafeを使っていなくても、呼び出したsafeなAPIの内部実装に欠陥があれば、メモリ破壊は起こりえます。#![forbid(unsafe_code)]は自分のクレートでの使用を禁止しますが、依存先まで欠陥がないことを証明するものではありません。

doc.rust-lang.org

2021年のcrossbeam-dequeには、競合によって同じタスクが二度取り出され、ヒープ上のタスクを二重解放しうる問題がありました。修正は0.7.4と0.8.1で提供されています。safeなAPIを提供するライブラリにも実装の点検と更新が必要です。

rustsec.org

CやC++のライブラリをFFI、別の言語の関数を呼び出す仕組みで使う場合も、呼び先にRustの借用検査は及びません。Rust側で引数の寿命や所有権を正しく扱っていても、呼び先の内部にあるメモリ破壊は残ります。移行時には、Rustへ書き換えたコードだけでなく、引き続き呼び出す外部ライブラリも確認します。

メモリ安全でも情報漏えいやサービス停止は起こる

メモリを正しく扱うプログラムでも、他の利用者のデータを返したり、同じ注文を二重に確定したりすることはあります。Rustのメモリ安全性はその利用者にデータを見せてよいか、その注文を受け付けてよいかまでは判断しません。

所有権は請求書を見る権限を意味しない

たとえば、ログイン済みの利用者が/invoices/202へアクセスすると、請求書202を返すAPIを考えます。IDを整数へ変換し、SQLに安全に渡し、取得結果を正しい型へ変換できていても、その請求書を利用者が閲覧できるかは別途確認が必要です。誰であるかを確かめるのが認証で、何をしてよいかを確かめるのが認可です。

検証コードではDBを省き、メモリ上の請求書を検索します。テナントは同じサービスを利用する別々の顧客組織です。次の型と検索関数は、すべてsafe Rustで書けます。

#[derive(Debug, PartialEq, Eq)]
pub struct Invoice {
    pub id: u64,
    pub tenant_id: u64,
    pub description: &'static str,
}

pub fn find_invoice_by_id(invoices: &[Invoice], invoice_id: u64) -> Option<&Invoice> {
    invoices.iter().find(|invoice| invoice.id == invoice_id)
}

pub fn find_invoice_for_tenant(
    invoices: &[Invoice],
    tenant_id: u64,
    invoice_id: u64,
) -> Option<&Invoice> {
    invoices
        .iter()
        .find(|invoice| invoice.id == invoice_id && invoice.tenant_id == tenant_id)
}

最初の関数は、IDが合えば請求書への参照を返します。後の関数は、請求書が認証済み利用者のテナントに属するかも調べます。戻り値のOptionは、見つかればSome、見つからなければNoneです。テナント1として、テナント2に属する請求書202を検索すると、出力はこうなります。

authenticated tenant: 1
ID-only lookup: Some(Invoice { id: 202, tenant_id: 2, description: "Tenant 2 private invoice" })
tenant-scoped lookup: None

ライブラリには#![forbid(unsafe_code)]を指定しています。この認可漏れを確認するテストは、Miriでも成功しました。この実行ではメモリの不正使用が検出されず、他の顧客のデータを返しています。このテストでは、意図的に「漏れること」を期待値にしています。メモリを安全に扱うことと、返してよいデータを選ぶことは、別々に確かめる必要があります。

Miriでテストが通ることは、調べていない入力や実行経路まで検査したことを意味しません。また、テストは与えた期待値との一致を調べるので、その期待値自体が正しいかは別に確認します。この例が示すのは、検査に通ったという結果だけでは、利用者に返してよいデータかを判断できないことです。

github.com

修正後の関数にも前提があります。tenant_idは認証済みの利用者情報から得なければなりません。攻撃者がリクエストで指定した値をそのまま渡せば、他テナントのIDを名乗れてしまいます。実際の認証処理はこの小さなサンプルの対象外です。

api-security.owasp.org

認可を確かめた後だけ作れる値を用意し、データを返す関数がその値を受け取るようにすれば、確認を飛ばすコードを書きにくくできます。ただし、値を作る処理が正しい利用者と対象を検査すること、権限が変わった後に古い判定を使い続けないことは、自分たちで設計します。

請求書を顧客同士で共有できるサービスなら、テナントIDの一致だけでは認可の条件を表せません。誰が誰に共有を許したか、管理者には何を見せるかも決める必要があります。これらの条件を実装し、許可する操作と拒否する操作の両方をテストします。

データ競合を防いでも二重処理は起こりうる

残り1個の在庫を、2つのリクエストがそれぞれ確認してから購入を確定する場面を考えます。個々のメモリアクセスが安全でも、「在庫確認」と「在庫を減らす」を一体の操作として扱わなければ、二重に販売する可能性があります。

Rustが防ぐデータ競合と、処理順序による競合は区別が必要です。DBのトランザクションや条件付き更新、再送に対する重複防止は、Rustでも設計します。デッドロックもメモリ安全性の保証では防げません。

doc.rust-lang.org

標準ライブラリにもコマンド注入の実例がある

2024年のCVE-2024-24576は、Windowsでstd::process::Commandからバッチファイルを実行する際の問題でした。信頼できない引数のエスケープが不十分で、任意のシェルコマンド実行につながりえました。対象はWindows上の該当する呼び出しです。修正はRust 1.77.2で提供されました。

blog.rust-lang.org

その後、ファイル名末尾の空白やピリオドをWindowsが取り除く動作によって、対策を回避できる条件も見つかりました。追加修正は1.81.0です。

blog.rust-lang.org

どちらも歴史的な修正済みの事例です。型やメモリを正しく扱っていても、同じ文字列をOSや別の処理系が異なる意味で解釈すると、脆弱性につながります。RustのコードからSQL、シェル、パス、URLを扱うときも、受け取る側での意味を考える必要があります。

メモリ不足で安全に止まっても、サービスは止まる

Rustの通常のstdを使う構成では、メモリ確保失敗に対する標準の動作は、メッセージを出してプロセスをabortすることです。try_reserve()など、失敗を値として扱えるAPIもありますが、プログラム全体のメモリ消費を自動で制限する機能ではありません。

doc.rust-lang.org

doc.rust-lang.org

panic時の巻き戻しとabortも違います。通常の巻き戻しでは値の破棄が行われますが、abortではその処理は走りません。また、値を破棄できても、すでに外部へ送った通知や確定した更新まで巻き戻るわけではありません。障害後に再試行してよい範囲は別途決める必要があります。

2024年のh2では、HTTP/2のCONTINUATIONフレームを処理し続ける問題が報告されました。CPU消費と遅延の増大につながります。Tokioのタスク実行量の制御により全面停止は抑えられましたが、サービスの劣化は起こります。修正は0.3.26と0.4.4で提供されています。

rustsec.org

入力のサイズや複雑さ、同時実行数、キューの長さ、実行時間を制限する仕事は、Rustへ移行しても残ります。メモリ破壊を防いでも、CPUやメモリを使い切れば、ほかのリクエストに応答できなくなります。

Rustへ移すメリットとデメリット

Rustにも脆弱性があることと、Rustへの移行で防げる不具合があることは、両立します。解放後の参照を使うコードは、safe Rustの借用検査で拒否されます。これは特定の入力で異常が起きたかを調べるテストとは異なり、参照と所有者の関係をコードから検査した結果です。次の変更で同じ誤りを入れても、再びコンパイラが拒否します。人が書いたコードにも、AIが生成したコードにも、この検査は働きます。

移行で得られる効果は、今どの言語で何を実装しているかによって変わります。Cで手動管理していたメモリをRustの所有権に従って扱う変更と、PHPの業務処理をRustの拡張へ移す変更では、取り除ける不具合が違います。

比べること Rustへ移すメリット デメリットと残る確認
メモリ管理 safeなAPIを使うコードでは、解放後の参照などを拒否できる。初期化や破棄の確認をライブラリへ集められる 所有者やデータの受け渡し方を設計し直す作業が必要。残したCやunsafeの実装は引き続き確認する
型とエラー処理 値の種類や成功・失敗を型で表し、型に合わない使用をコンパイル時に見つけられる 既存の型変換、欠損値、例外の扱いを調べて作り直す必要がある。型が合っていても認可や業務ルールは誤りうる
実行性能 CPUでの計算に時間がかかる処理では、実行時間やメモリ使用量を改善できる可能性がある 言語間のデータ変換やコピー、通信の費用が加わる場合がある。Rustの関数だけ速くても、リクエスト全体が速くなるとは限らない

性能を理由に移すなら、先に時間がかかっている処理を測ります。データベースの応答待ちが大半なら、計算部分だけを速くしても改善は限られます。Rust単体の測定に加え、既存のアプリケーションから呼び出した状態で、応答時間とメモリ使用量を比べます。

移行中は既存のテストに加え、Rustのビルドと検査もCIへ組み込みます。所有権に沿った設計を習得し、対象OS向けのビルドやライブラリの組み込み、配布物の更新手順を整える作業が増えます。新旧の実装が並ぶ間は、両方の依存更新と、接続部分の検証も必要です。書き換えにかかる時間だけでなく、切り替え後に修正を届けるまでの作業も比べます。

livebook.manning.com

誰がどの作業で困っているのかを調べる

直近の変更や障害対応を、実際に作業したメンバーと振り返ります。調査が長引いた箇所や、他の人の確認を待った箇所を、修正を配備するまでたどります。先にRustの採用を決めてしまうと、言語で解決できる問題ばかりを探し、担当の分担や配備の仕組みに原因がある問題を見落としやすくなります。

livebook.manning.com

たとえば、Cで書いた入力解析のメモリ破壊が繰り返され、開発者が再現と修正に追われているとします。その処理をsafe Rustへ移して原因を取り除けるなら、同じ種類の不具合を調べ直す仕事を減らせます。守れるデータや処理に加え、誰がどの調査から解放されるのかまで説明できます。

一方、修正はできているのに、共有ライブラリの更新を別チームへ依頼して待っているなら、言語を替えても待ち時間は残ります。Rustに詳しい人だけへレビューや調査が集中すれば、新しい待ち時間も生まれます。更新を担当する範囲や配備の権限を見直すことが、この問題の解消につながります。同じ「開発が遅い」という困りごとでも、原因によって変えるべきものは違います。

導入後に保守するメンバーは、対象や方法を決める段階から参加します。今の実装に詳しい人の知識と、Rustに詳しい人の知識を合わせ、引き継ぐ動作や更新手順を確かめます。新しい実装を受け取る人が、学習や引き継ぎに使える時間も必要です。導入を提案した人の作業が楽になっても、別のチームへ調査や保守を移しただけなら、何が改善したのかを問い直します。

www.manning.com

どの処理をRustへ移すか

PHP2Shellで問題になったのは、PHP処理系を実装するCのコードです。mod_phpがリクエストを解析した後にRustの拡張を呼ぶ構成では、Rustが動く前の攻撃経路が残ります。そのため、業務処理をRustへ移しても、使い続けるPHP処理系の更新は必要です。

www.php.net

メモリ破壊を減らす目的なら、外部入力を受け取ってから、どのコードが解析し、どこでデータを保持・解放するかをたどります。問題のあるCの関数をFFI経由で呼び続けても、その内部のメモリ管理に借用検査は及びません。unsafeで同じポインタ操作を再現する場合も、寿命や初期化の条件を実装者が確認します。どの処理をsafeなAPIへ置き換え、古い実装をどの実行経路から外せたかが、移行の効果を確かめる手掛かりになります。

既存システムの一部だけを置き換える方法もあります。たとえば、入力の解析や集計を切り出せるなら、その処理をRustへ移し、入出力や画面は既存の実装を使い続けます。変更する範囲を絞ると、不具合が出たときに比較するコードも絞れます。

CとRustの接続部分を設計する

Cのコードを残すなら、渡したメモリを誰が解放し、いつまで参照できるかを決めます。Rustの借用検査はCの内部には及びません。別プロセスへHTTPなどで値を渡せばポインタの共有を避けられますが、通信の失敗や再試行を扱う作業が増えます。接続方式も、保守するメンバーが不具合を調べて直せるかまで含めて選びます。

doc.rust-lang.org

既存のテストで置き換えを確かめる

既存のテストが呼ぶ関数やAPIを保ち、その内部だけをRustへ差し替えると、移行前から必要だった動作を引き続き検査できます。さらに、同じ入力を旧実装とRustの実装へ渡し、結果の違いを調べる差分テストも使えます。正常な入力だけでなく、空文字、欠けた項目、数値の上限付近、不正な形式を含めます。言語間で文字列や数値の扱いが変われば、メモリ安全なコードでも結果は変わるためです。

livebook.manning.com

比較のために処理を二度動かしてよいかも確認します。解析や計算なら出力を比べやすい一方、請求の確定やメール送信を本番で二度実行すると、比較そのものが不具合になります。外部への書き込みを切り離したテスト環境で比較するなど、確かめる方法を処理に合わせます。

比較で一致させる出力も点検します。旧実装が別テナントの請求書を返していたとします。その出力を正解として新しい実装を作れば、認可漏れも忠実に引き継げます。先ほどの検証でも、他社の請求書が返ることを期待するテストは成功しました。旧実装との一致だけでは、認可漏れまで引き継いだことに気づけません。

引き継ぐ条件は関数名や内部の型に依存しない形でも残します。請求書なら「テナント1としてテナント2の請求書を要求しても、内容を受け取れない」という条件です。実装言語がPHPからRustに変わっても、APIへの入力と応答を使って検査できます。正常な応答だけでなく、権限のない要求を拒否すること、中断後に再試行しても二重に処理しないことを確かめます。

長く保守されたコードには、過去の障害を受けて加えた制限や、依存先の例外に対処する処理が残っています。書き直すときは、その処理を設けた理由と、今も必要な条件を取り出します。たとえば、テナントIDをリクエストから信用してはいけない理由を残しておけば、再実装でも認証済みの利用者情報から取得する必要性が分かります。コードを短くするために検査を消すのか、別の検査へ置き換えるのかを判断できます。

AIへ仕様を渡して再生成するときも、この作業は省けません。自然言語で書いた要求は認可の例外や失敗時の動作まで網羅しているとは限りません。同じ要求の読み違いからコードとテストを作れば、両方が一致したまま誤った動作を残せます。期待値は生成されたコードだけから決めず、利用者に許可する操作や、過去の障害で守ることになった条件から確かめます。

AIが要求を解釈する処理と、Rustコンパイラが型や借用の規則を検査する処理では、確かめていることが違います。生成したコードをコンパイルし、必要な動作のテストを通し、配備後の応答やエラーも確認します。実装を作る時間が短くなっても、置き換えを受け入れる条件は省けません。

切り替えと旧実装の廃止まで確かめる

置き換えた処理は実際の配備先でも確かめます。まず対象を限定して切り替え、エラー率や応答時間、メモリ使用量を確認してから範囲を広げます。切り戻しに備え、新しい実装が書いたデータを旧実装で読めることと、戻した後に二重処理が起きないことをテストします。既知の脆弱性がある実装へ戻すと攻撃経路も戻るため、切り戻し先にも必要な修正を適用しておきます。

新しい実装が動いても、古い実装へアクセスする経路が残れば、そちらの不具合は攻撃に使われます。切り戻しのために残す範囲と期間を決め、不要になった経路、認証情報、配布ジョブ、依存を削除します。移行した部分で減らせるはずの不具合が、稼働中のサービスから取り除かれたかまで確かめます。

言語を選んだ後にもインシデントは起きる

私はインシデントが発生することを前提に、システムを作り運用すべきだと考えています。言語ごとに防げる不具合は違いますが、認可の漏れ、依存パッケージの侵害、設定の誤りは、どの言語でも起こりえます。ログを調べて侵害に気づき、危険な機能を止め、修正を配備できるようにしておく必要があります。

これはどの言語を使っても危険は同じだという意味ではありません。言語の仕組みで不具合の種類を減らせれば、その原因に対する調査や緊急修正も減らせます。ただし、残る原因によるインシデントには備えます。予防で発生しにくくすることと、発生後の被害を抑えることは、両方必要です。

設計時には、誰が、どの経路で、何をできるのかを整理します。たとえば、請求書APIの認可が抜けたら、どの顧客のデータを読まれるのか。ビルド用の認証情報を盗まれたら、どの配布物を書き換えられるのか。想定する攻撃ごとに、既存の対策で防げることと、追加の対策が必要なことを調べます。こうした検討が脅威モデリングです。外部の利用者だけでなく、管理者のアカウントや自動実行するジョブも対象に含めます。

移行前後で同じ攻撃を想定して確認すれば、言語を替えた効果と、ほかに必要な対策を区別できます。

依存するコードの出所も確認する

攻撃はメモリ破壊に限りません。2024年のxzのバックドアは、配布物やビルドの過程に入り込みました。2025年のnpmのShai-Huludでは、侵害されたメンテナーのアカウントを通じて、悪意あるインストール後スクリプトが配布されました。アプリケーションのソースに不具合がなくても、取り込む依存が改ざんされていれば、ビルド時や実行時に攻撃者のコードが動きます。

www.openwall.com

github.blog

RustでもCargoは依存パッケージのbuild.rsをコンパイルして実行します。そこでネイティブライブラリをビルドすることもあります。メモリ安全性は取り込んだコードの善意や、ビルド環境にある秘密情報の保護を保証しません。メモリを正しく使いながら、秘密情報を外へ送るプログラムは書けます。

doc.rust-lang.org

USENIX Security 2025の研究は、コード生成モデルが存在しないパッケージ名を提案する現象を調べています。攻撃者がその名前を登録して導入を狙う手法は、スロップスクワッティングと呼ばれます。この研究は現在の全モデルの発生率を示すものではありませんが、AIの提案でもパッケージの実在と出所を確かめる理由になります。

www.usenix.org

ロックファイルでバージョンを固定しても、その内容が悪意あるコードなら被害を防げません。npm provenanceは、パッケージの元になったソースやビルドを確かめる仕組みですが、公式資料も悪意あるコードがないことの保証にはならないと説明しています。バージョンと出所の確認に加え、変更内容も調べます。

docs.npmjs.com

修正と検知にかかる時間と被害範囲を確かめる

言語やテストで不具合を防ぐことに加え、発生後の対応についても次の3つを確かめます。

評価すること 確認したい状態
問題を知ってから修正を配備するまでの時間 影響のある稼働環境を特定し、修正を検証し、古い実行物を入れ替えられる
侵害に気づくまでの時間 不審な操作や通信を観測し、調査を始められる
侵害されたときの被害範囲 1つのサービスや認証情報の侵害が、全顧客や配布基盤へ広がりにくい

修正にかかる時間には、影響の調査を始めるまでの待ち時間も含めます。パッチを作るのが速くても、未調査の通知が積み上がっていれば、その間は脆弱な環境が残ります。影響の判定、修正の検証、配備のどこで時間を使っているかを分けて見ると、改善する対象を選べます。

完了は稼働中の環境へ修正が届いた時点で確かめます。Rustの標準ライブラリや静的に組み込む依存を更新するなら、再ビルドと配備まで必要です。修正版がまだなければ、隔離や機能停止までの時間と、修正版を配備するまでの時間を分けて記録します。

修正の配備と、更新前に侵害が起きていないかの調査も別の作業です。PHP2Shellでも、修正済みの処理系へ入れ替えたことで、すでに盗まれた認証情報が無効になるわけではありません。更新済みの環境と未更新の環境を記録します。調査についても、ログで確認できた操作と、記録の不足で調べられない操作を分けて残します。

侵害に気づくまでの時間には見落としている事案を含められません。検知できた事案だけの平均が短くても、記録されない操作が多ければ、見落としは残ります。どの操作のログがあるか、演習でどの操作を検知できなかったかも確認します。

稼働中のサービスと依存の一覧を保つ

SBOM、ソフトウェア部品表には、取り込んだライブラリなどを記録します。そのファイルだけでは、どのサービスで使われ、何を更新すればよいかまでは分かりません。稼働中の実行ファイルやコンテナ、サービス名、依存、更新するチームを対応づけます。Log4Shellへの共同勧告でも、CISAなどはLog4jを使う資産の棚卸しを求めています。

https://www.cisa.gov/sites/default/files/publications/AA21-356A_Joint_CSA_Mitigating_Log4Shell_and_Other_Log4j-Related_Vulnerabilities.pdfwww.cisa.gov

PHPのCLIだけを調べると、別のコンテナで動くFPMを見落とすことがあります。RustのCargo.lockだけでは、同梱するCライブラリを確認しきれない場合もあります。ソース上の依存を更新していても、配備されていなければ古い実行物が動きます。一覧には稼働中の実行物と確認した時刻を記録し、何を調べた結果なのかを残します。

通常の依存更新と緊急修正を分ける

通常更新では、公開直後の取り込みを一定期間待つ方法があります。RenovateのminimumReleaseAgeは、更新を提案するまでの待機期間を設けます。ただし、期間を過ぎれば安全になるわけではありません。直接使うライブラリがさらに取り込む推移的依存まで制御できるかも、パッケージマネージャーと設定ごとに確認します。

緊急のセキュリティ修正には、待機期間を迂回する経路が必要です。Renovateは、検出したセキュリティ更新を待機の対象外にすると説明しています。利用する設定でその例外が働き、後続の承認や配備でも止まらず、修正版を届けられるかまで確かめます。

docs.renovatebot.com

攻撃の条件と被害から更新の順序を決める

依存の既知脆弱性を調べるSCAを導入しても、検出件数だけでは更新の順序を決められません。まず、該当機能を使っているか、外部から到達できるかを確認します。次に、その処理の権限で何を読み取り、何を書き換えられるかを調べます。同じ脆弱性でも、外部公開するAPIと隔離した開発環境では、攻撃の成立条件と被害が異なります。普段から公開範囲と権限を記録しておくと、調査を始めやすくなります。

補助情報として、EPSSは公開CVEが今後30日間に実際の攻撃で悪用される確率を推定し、CISAのKEVは悪用が確認された脆弱性を集めています。前者は自社固有の被害額や到達可能性を示す数値ではありません。後者に未掲載でも、悪用されないと判断できるわけではありません。

www.first.org

github.com

到達可能性の分析でも、読み込ませていない設定や解析できない呼び出しがあれば、攻撃の経路を見落としえます。「解析では見つからない」と「到達しないことを確認できた」を分けます。自社に影響する認証前RCEのような問題では、スコアや掲載の更新を待たず、確認できた影響を基に対応を進める必要があります。

小さな更新を配備しCIの権限を絞る

RenovateやDependabotが更新を提案できても、テストと配備が進まなければ、脆弱なバージョンが動き続けます。普段から小さな更新を配備し、権限のない操作を拒否することや、中断・再試行でデータを壊さないことをテストします。何年分もの更新を一度に適用すると、互換性の確認や設定変更も重なります。小刻みに更新すれば、緊急時に調べる差分を減らせます。

調査した依存は一覧へ反映し、再現できた不具合は回帰テストに残し、繰り返した配備操作は自動化します。自動化したジョブにも、配備先、実行したバージョン、失敗した箇所を記録します。次の対応で同じ場所を探し直す作業を減らし、途中から再実行してよいかも判断できるようにします。

CIで自動実行するビルドやテスト、配備に使う認証情報も、侵害された依存コードから利用される可能性があります。GitHub Actionsとクラウドの連携では、OIDCを使い、長期間有効な秘密情報を保存する代わりに、ジョブ向けの短命な認証情報を取得できます。どのリポジトリや実行条件を信頼するかも設定する必要があります。

docs.github.com

短命な認証情報でも、有効な間は悪用される危険が残ります。信頼できないコードを動かすジョブと、配布するジョブを分離し、権限や外向き通信を絞ります。有効期限を短くすれば、盗まれた認証情報を使える時間が短くなります。ジョブを分離すれば、ビルド中のコードから配布用の認証情報に触れにくくできます。権限を絞れば、盗まれたときに読み書きできる対象を減らせます。これらを組み合わせて被害を抑えます。

AIへ一次トリアージを移す

トリアージは、入ってきた情報から影響と緊急度を見極め、何を先に調べ、対応するかを決める作業です。私はセキュリティ対応では、この作業が件数の多さと調査の難しさの両方で詰まると考えています。複数の弱点や権限を組み合わせる攻撃では、対象バージョンだけを見ても影響を判断できません。構成、呼び出し経路、実際の操作まで調べる必要があります。

自社のコードを変更していなくても、依存先の不具合や攻撃方法が公開されれば、調査の緊急度は変わります。私はAIがこの変化を大幅に速め、調べ終えるより速く新しい問題が届くことを懸念しています。PHP2Shellでも、研究者はAIを調査に利用した過程を報告しています。PHP Foundationも、AIに関連する報告への対応と修正方針の検討について説明しています。

calif.io

thephp.foundation

どの規模の問題がどの頻度で見つかるかは、まだ見通せません。それでも、一定期間に調べ終える件数を新しく届く件数が上回り続ければ、未調査の案件は増えます。一件ごとに読む資料や照合するログが増えれば、処理できる件数も減ります。人が全件を最初から調べる方式のままでは、能力以前に使える時間が足りなくなります。私は対応できる種類の案件から、証拠集めと一次分類をAIへ移す必要があると考えています。

一次対応で集める情報を決める

脆弱性の公開情報を読むときは、自社に影響する条件があるかを調べます。侵害の疑いを示す通知を読むときは、何が起き、被害が続いているかを調べます。脆弱なバージョンの使用と、侵害を示す操作の観測は別の情報です。AIへ渡す際も、何を判断するための調査かを指定します。

AIに進めさせる作業 判断のために残す情報
同じ事象を示す通知をまとめる 元の通知、観測時刻、同じ事象と考えた根拠
公開された影響条件を稼働環境と照合する 実行物のバージョン、設定、確認した時刻、未確認の条件
関連する操作や通信を調べる 調べた期間と対象、元ログへの参照、観測した事実と推定の区別
優先順位と次の調査を提案する 想定する被害、優先する理由、追加で必要な証拠

バージョンの照合や定型のログ検索は、既存のツールで再現できる形にします。AIには、その道具を使った調査と、説明文や複数の記録を照合する作業を任せます。「問題なし」という短文だけを返させると、人は判断を確かめるために最初から調べ直すことになります。元の証拠と未確認の条件が残っていれば、次の調査から始められます。

対象を限定した自動分類は、すでに提供されています。MicrosoftのPhishing Triage Agentは、利用者が報告したメールの内容やURL、関連する脅威情報などを調べます。誤報と判定したアラートは解決済みにし、悪性と判定したものは人が調査・対応できるよう未解決のまま残します。これは報告メールの仕分けの例であり、あらゆるインシデントを自動判断できるという意味ではありません。

learn.microsoft.com

人へ戻す条件と見逃しを確かめる

AIの結果を人が全件、最初から確認し直すなら、調査の待ち時間は残ります。私なら、まず過去の事案を使い、当時得られた情報だけで影響と緊急度を判定させます。見逃しや誤った根拠を調べ、確認不要とする条件を検証できた種類から自動処理を広げます。重大な侵害の疑い、証拠の矛盾、情報不足は、人の調査へ引き継ぐ条件にします。モデルや接続先、参照するログを変えたときも、過去の事案で再評価します。

特に「該当する操作がない」と「ログを取得できなかった」は区別します。調査の失敗や未完了を、問題なしに変換してはいけません。問い合わせの失敗、権限不足、情報の古さを結果に含め、どこまで確認できたかを明示します。Microsoftの公式資料も、不完全な出力や処理遅延などの限界を説明しています。

learn.microsoft.com

効果は分類件数だけでなく、重大な案件が人へ届くまでの時間、未判定のまま残った件数、人が根拠を確認・訂正する時間で見ます。AIが対応不要とした案件も抽出して調べ、見逃しを確認します。要約を作るのが速くなっても、誤った判断の訂正にそれ以上の時間がかかれば、調査は進みません。

AI自身が容量の上限や接続先の障害で止まることも想定します。受け付けた案件と処理を終えた案件を照合し、未完了を見失わないようにします。自動処理が止まったときも、重大な疑いを優先して人へ渡す経路を残します。人の確認が必要な案件を、優先順位に沿って届けられるかが、一次対応を移す際の確認点です。

調査する権限と本番を変更する権限を分ける

AWS DevOps Agentはログやエラー率、直近のデプロイなどを照合し、障害の原因候補と対処案を示します。観測データやアクセス範囲が不足すれば、推定を誤る可能性があります。この調査支援の機能から、セキュリティアラートを正しく自動終了できるとは判断できません。

docs.aws.amazon.com

設定を変更するdirected actionsは既定で無効です。有効化とAWSの権限管理機能IAMの設定に加え、変更の実行には対象と操作を限定した承認が必要です。操作履歴はCloudTrailで確認できます。変更を任せるなら、変更後の状態も確かめ、問題が出たときに止めたり戻したりする手順を用意します。APIの呼び出しに成功しても、障害が直ったとは限りません。

docs.aws.amazon.com

AIが読むログには攻撃者の入力も含まれます。それを指示として受け取って操作を誘導される、プロンプトインジェクションにも対処が必要です。AWSもログやリソースのタグを通じた攻撃を挙げています。接続するツールの権限を絞り、AIが誘導されても実行できる操作を制限します。

docs.aws.amazon.com

危険な機能を止めて復旧できるようにする

他社の請求書を高速に返すAPIは、HTTPの成功率だけを見ると正常に見えます。応答を返し続けていても、そのたびに情報が漏れるなら、止める必要があります。

SLOは操作の成功率や応答時間など、利用者がサービスを使えるかを測る指標に定める目標値です。ただし、可用性の目標を達成していることは、情報が漏れていないことの証明にはなりません。攻撃に使われる入力を遮断する、影響のあるワーカーを隔離するなど、セキュリティ上の理由で停止する条件も決めておきます。

サービス全体を止めずに、危険な入力を受け付ける機能だけを止められる場合もあります。どの入口を遮断すれば攻撃される処理へ届かなくなるかを、平時に確認します。PHP2Shellでは、アプリケーションが動く前に入力の解析が始まります。アプリケーションでメンテナンス画面を返しても、その前の処理を止められるとは限りません。

再開前には、修正済みのバージョンが動いていることを確認します。隔離した古いワーカーが処理へ戻っていないことも確認します。Webリクエストを検査・遮断するWAFを使いながら更新を進める場合も、どの攻撃を遮断でき、何が残るかを調べます。WAFのルールを入れたことと、不具合のあるコードを更新したことは別です。

PHP2ShellへのWAFの保護を提供するCloudflareも、PHPの更新を引き続き求めています。

blog.cloudflare.com

復旧に使う管理画面や認証サービスも、障害や侵害の影響を受けます。停止したサービスにログインしなければ再起動できない手順では、作業を始められません。停止や隔離の操作には、復旧対象に依存せず使える手段が必要です。その手段にも認証・認可と操作の記録を残し、攻撃者が復旧用の経路を悪用できないようにします。

演習では、停止対象に障害が起きた状態で管理操作を試します。侵害されたアカウントを使わずに隔離できることも確かめます。監査ログは侵害された権限で消せない場所に残せるかを確認します。手順書のコマンドを実際に必要になる状況で動かすと、足りない権限や依存を見つけられます。

誰の困りごとを減らすために選ぶのか

私は、誰が何に困っているのかを説明できて初めて、Rustを導入する判断ができると考えています。メモリ破壊の調査を繰り返している開発者なのか、他チームの作業を待たないと更新できない運用担当者なのか。その人たちが困る原因までたどれば、言語を替えるべきか、担当する範囲や更新手順を変えるべきかを判断できます。

AIへ一次トリアージを移すのも、全件を人が調べる方式では、必要な調査が追いつかなくなるからです。導入後は調査を担う人が重大な案件へ早く取りかかれるようになったかを確かめます。RustやAIの採用だけでは、困っていた仕事が改善したとは判断できません。

ノリでRustを導入すると、普通に運用の負担が増えます。ビルドも依存の更新も、障害時の調査も、誰かが続ける仕事です。導入を決める人と、その仕事を担う人が違うなら、後から引き継ぎを頼むだけでは足りません。どこを移すか、何を学ぶか、誰とレビューするかを、保守するメンバーと一緒に決めたいです。

私なら、本番への導入を決める前に、そのメンバーと依存の更新から再ビルド、テスト、検証環境への配備までを一度通します。不具合の調査や復旧も試し、詳しい人が不在でも進められるかを確かめます。うまく進まなければ、学習や共同作業の時間を増やすか、移行する範囲を小さくします。人が慣れることだけに期待して、扱う技術を増やし続けたくはありません。

まず、誰がどう困っていて、その人たちの仕事をどう変えたいのかをちゃんと話そう。そのうえでRustが合うなら、修正して配備し続けられるところまで一緒に作ろう。「Rustなら安心っしょ」で本番に入れるのは、な、な、やめておこうな!