プロンプトと適用範囲
アーカイブ展開処理は、ユーザーがアップロードしたファイルを /srv/uploads/job-42 に書き込みます。アーカイブのエントリ名は呼び出し元によって完全に制御されており、../../etc/passwd、絶対パス、またはジョブディレクトリ外を指すシンボリックリンクが送信される可能性があります。サービスは、すべての操作が確実にジョブディレクトリ内に留まるようにしながら、ディレクトリとファイルを作成する必要があります。
Go 1.24以降を使用してください。通常のアプリケーション権限および本番環境でのLinuxを想定し、WindowsまたはWASIクライアントの可能性も考慮します。信頼できないファイル名と、任意の出力ディレクトリを明示的に選択することが許可されている呼び出し元とを区別してください。
面接官がテストしていること
- 文字列置換を行うのではなく、ディレクトリ制約付きAPIを選択する前に、ルートと攻撃者が制御する入力を定義できるかどうか。
os.Rootが..やルートを脱出するシンボリックリンクを拒否すること、およびその保証がfilepath.Cleanとどのように異なるかを把握しているか。EvalSymlinksでチェックしてから後で開く際におけるTOCTOUウィンドウを認識しているかどうか。Root、ファイル記述子、一時ファイル、クリーンアップ、および権限ポリシーを適切に処理できるかどうか。- バインドマウント、
GOOS=js、WASI実装、および非常に深いパスに関する制限を説明できるかどうか。
最初に明確にすべき質問
- 操作は読み取り、書き込み、またはその両方ですか?読み取り専用アクセスと作成では、必要な
OpenFileフラグと権限が異なります。 - ルートはサービスによって固定されていますか、それとも呼び出し元によって選択されますか?呼び出し元が任意のディレクトリを選択できる場合、
os.Rootは追加のサンドボックス境界としては機能しません。 - アーカイブ内でシンボリックリンク、ハードリンク、デバイスファイル、または名前変更の作成は許可されますか?許可されていないタイプは、
Rootに委ねるのではなく、アーカイブポリシーによって拒否する必要があります。 GOOS=jsやWASIターゲットはスコープに含まれますか?公式資料では、Unixの記述子ベースの実装とは異なるパス安全性保証が説明されています。
30秒の回答フレームワーク
「ジョブディレクトリを唯一のルートとします。すべてのアーカイブ名は相対名として os.Root に渡され、ベース文字列と結合して通常の os.Create を呼び出すことはしません。作成、オープン、削除は Root のメソッドを経由するため、.. や脱出するシンボリックリンクは失敗します。制限された権限を持つ一時名を介して書き込み、その後製品ポリシーに従ってコミットします。テストでは、トラバーサル、シンボリックリンクの競合、同時作成、Windowsデバイス名、ルートのクローズをカバーします。Root はパスを制約しますが、アーカイブタイプのチェック、権限の分離、またはバインドマウントの制御を代替するものではありません。」
ステップバイステップの詳細解説
1. セキュリティ不変条件を確定する
「名前に .. が含まれていない」というのはセキュリティ定義ではありません。攻撃者はシンボリックリンクを使用したり、チェックとオープンの間にディレクトリエントリを置き換えたりすることができます。不変条件を正確に述べてください:アーカイブ名から派生したすべてのファイルシステム操作はジョブルート内で解決され、それを証明できない場合は操作が失敗する、ということです。
Go 1.24の os.OpenRoot はルートディレクトリを開きます。Root.Open、Root.Create、Root.OpenFile、Root.Mkdir、Root.Stat などのメソッドは、そのルートに対する相対名を受け入れます。この実装は、.. およびルート外へのシンボリックリンクトラバーサルを拒否します。タスクには1つのルートを再利用し、すべてのファイルハンドルが閉じられた後にそのルートをクローズします。
func writeEntry(rootDir, name string, data []byte) error {
root, err := os.OpenRoot(rootDir)
if err != nil {
return err
}
defer root.Close()
f, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(data)
return err
}これにより名前解決がエスケープするのを防ぎますが、本番コードでは依然としてファイルサイズ、エントリ数、ディレクトリの深さの制限、および書き込み失敗後の不完全な出力の削除が必要です。O_EXCL は「既存のファイルを上書きしない」ことを意味し、アーカイブの重複排除やビジネス上の冪等性を意味するものではありません。
2. 文字列のサニタイズが不十分な理由を説明する
filepath.Clean は a/../b を正規化できますが、a がルート外へのシンボリックリンクではないことを証明することはできません。EvalSymlinks でチェックしてから後で開く場合、依然としてTOCTOUウィンドウが存在します。チェック後にディレクトリエントリが変更される可能性があるためです。filepath.Join の後に os.Create を続けても、ディレクトリ境界は提供されません。
Unix上では、Root はルートディレクトリ記述子と制約付き相対オープンを使用するため、境界は個別のチェックに依存するのではなく操作自体に関与します。ルート内部の相対パスとシンボリックリンクは許可されますが、../ やエスケープする絶対シンボリックリンクは失敗する必要があります。製品が明示的にサポートしていない限り、呼び出し元はアーカイブ内のシンボリックリンクエントリを引き続き拒否すべきです。
3. アーカイブのタイプと書き込みを処理する
書き込みを行う前に、各エントリのタイプ、サイズ、権限、および名前を読み取ります。製品がサポートしていないデバイスファイル、FIFO、ハードリンク、およびシンボリックリンクは拒否します。Root.Mkdir または MkdirAll で親ディレクトリを作成し、その後 Root.OpenFile で通常ファイルを作成します。合計バイト数、ファイルあたりのバイト数、パスコンポーネント、および同時実行ジョブに上限(バジェット)を設定します。
より安全な書き込みシーケンスは、「一時名 → 完全な書き込み → 検証 → アトミックコミット」です。一時名も Root を介して作成する必要があります。システムの一時ディレクトリに書き込んでディレクトリ間で移動すると、権限、マウント、およびアトミック性の前提が崩れます。使用可能な Root APIが必要な名前変更動作を提供できない場合は、無制限のパス操作に暗黙的にフォールバックするのではなく、文書化されたプラットフォームまたは製品の制約としてください。
4. プラットフォームと権限の境界を明記する
Goのドキュメントによると、Unix実装は通常ルートディレクトリ記述子を追跡するため、ルートの名前が変更されても元のディレクトリを参照し続けます。Windowsはハンドルを使用し、一部の予約済みデバイス名をブロックします。GOOS=js には openat ファミリのシステムコールがないため、シンボリックリンクの検証にTOCTOUの制限が残ります。WASIの保証はその実装に依存します。また、Root はLinuxのバインドマウント、/proc 特殊ファイル、またはUnixデバイスファイルへのアクセスをブロックしません。
したがって、コンテナの分離、マウントポリシー、プロセスの権限、およびアーカイブタイプの許可リストは別個の制御となります。Root を完全なコンテナサンドボックスとして説明したり、呼び出し元が選択した任意のディレクトリを制約付きルートとして扱ったりしないでください。
5. 実行可能な検証を構築する
../escape、絶対パス、ルート内部のシンボリックリンク、ルート外のシンボリックリンク、../ で終わる名前、同一ファイルの同時作成、サイズ超過エントリ、およびルートを閉じた後の呼び出しをテストします。Go 1.24.3では、../ で終わる Root パスが親ディレクトリを開いてしまう可能性がある問題が修正されたため、CIでは修正を含むツールチェーンを固定し、そのリグレッションテストを保持する必要があります。
Linuxでは、承認ケースと拒否ケースの両方に対して一時ディレクトリと実際のシンボリックリンクを使用します。共有状態の競合には -race を使用しますが、これをファイルシステム境界の証明として扱わないでください。クロスコンパイルされたバイナリは、同一のカーネル保証を証明するものではありません。テストマトリックスに各 GOOS を含めてください。
質の高い模範解答
「ジョブディレクトリを固定ルートとし、信頼できないアーカイブ名は os.Root に渡される相対パスとしてのみ許可します。エクストラクタはデバイスファイル、ハードリンク、サポートされていないシンボリックリンクを拒否し、エントリ数、バイト数、ディレクトリの深さを制限します。Root を介して親ディレクトリと通常ファイルを作成し、同じルート配下の一時名に書き込み、失敗時には不完全な出力を削除します。.. やエスケープするシンボリックリンクは、制約付きオープン自体によって拒否されるため、チェックして後で開くことによるTOCTOUウィンドウを回避できます。
これを完全なサンドボックスとは呼びません。バインドマウント、権限、コンテナマウント、およびアーカイブポリシーは依然として別個のものです。Linuxテストでは、トラバーサル、重複作成、クリーンアップ、末尾の ../ に対して実際のシンボリックリンクと同時書き込みを使用し、Windows、WASI、および GOOS=js には明示的な境界テストを設けます。製品が呼び出し元による任意のディレクトリ選択を許可している場合は、誤った Root の前提を排除し、明示的な権限および監査ポリシーを使用します。」
よくある間違い
- 兆候:
filepath.Join(base, name)の後に通常のos.Createを実行する → 失敗する理由:..やシンボリックリンクがbaseの外側で解決される可能性がある → 修正方法: ルートを固定し、すべての操作をRoot経由で実行する。 - 兆候:
EvalSymlinksでチェックしてから通常通り開く → 失敗する理由: TOCTOUウィンドウが残る → 修正方法: 制約をオープン処理の一部とし、競合の境界をテストする。 - 兆候:
os.Rootがすべてのファイルシステムエスケープをブロックすると主張する → 失敗する理由: バインドマウント、デバイスファイル、および権限はそのAPI保証の範囲外である → 修正方法: マウント、権限、およびアーカイブタイプの制御を追加する。 - 兆候:
GOOS=js、WASI、およびパッチバージョンを無視する → 失敗する理由: プラットフォームの実装とセキュリティ修正が異なる → 修正方法: ツールチェーンを固定し、クロスプラットフォームのリグレッションマトリックスを構築する。 - 兆候: 抽出の成功のみをテストする → 失敗する理由: パスの安全性は、拒否されたエスケープとクリーンアップによって証明される → 修正方法: トラバーサル、絶対名、シンボリックリンク、競合、制限、およびクローズされたルート呼び出しをテストする。
フォローアップの質問と回答
フォローアップ 1: アーカイブ内のルート内部シンボリックリンクは許可すべきですか?
製品要件に基づいて決定します。リンクを保持する必要がある場合は、Root を介してターゲットがルート内に留まっていることを確認し、リンク数と深さに上限を設定します。ジョブが通常ファイルのみを展開する場合は、シンボリックリンクを拒否する方が監査が容易です。いずれにしても、Root はアーカイブタイプの許可リストの代わりにはなりません。
フォローアップ 2: Linuxのバインドマウントはどのように処理しますか?
Root はバインドマウントの分離を提供しません。コンテナまたはホストのマウントポリシーを通じてジョブディレクトリを専用のファイルシステムに配置し、アプリケーションの権限を制限し、デプロイメントチェックで信頼できないマウントを拒否します。脅威モデルに特権を持つ攻撃者が含まれる場合は、文字列チェックを追加するのではなく、より強力なサンドボックスまたは隔離されたワーカーを使用してください。
フォローアップ 3: システムの一時ディレクトリに一時ファイルを書き込んでからルートに移動してはいけない理由は何ですか?
ディレクトリをまたぐ移動は、権限、マウント、およびアトミック性の前提を変更し、一時ファイルがリークする可能性があります。Root を介して一時名を作成し、同じルート配下で書き込みとコミットを完了してください。プラットフォームが必要なアトミック操作を提供できない場合は、暗黙的に通常のパス結合に戻すのではなく、機能を縮小するか監査済みの互換性実装として文書化してください。
フォローアップ 4: GOOS=js も同じ安全性を持つと主張できますか?
いいえ。公式の説明によると、そのターゲットには openat ファミリが存在せず、シンボリックリンクの検証にTOCTOUの制限があります。そのターゲットに対して機能を制限するか、ランタイムのサンドボックスに依存するか、あるいは信頼できないファイルの処理をより強力なディレクトリ制約を持つサーバーに移動してください。APIとドキュメントでその違いを開示する必要があります。