Goエージェント開発日誌:os.Rootとパストラバーサル脆弱性について
コントラストブログを購読する
私たちのブログを購読することで、最新のappsecニュースをすべて把握し、ベストプラクティスを開発できます。また、最新のコントラスト製品ニュースとエキサイティングなアプリケーションセキュリティイベントについても通知されます。
2025年2月にリリースされた最新のGoリリースであるGo 1.24では、os.Root型という重要なセキュリティ強化が導入された。pe.
os.Rootは、Go 1.24よりGo言語の標準ライブラリの一部となったため、全てのGoユーザが自動的にこの機能を利用できるようになった。 os.Rootは、パストラバーサル攻撃に対する重要な防御層を提供するように設計されており、ファイルシステム操作を指定されたルートディレクトリ内に制限する。但し、どんなセキュリティ対策でもそうだが、その有効性は絶対的なものではない。os.Rootが依然として脆弱となる可能性のある特定のシナリオがあり、これらの特質を理解することは、堅牢なアプリケーションセキュリティにとって不可欠だ。
この記事では、こうした潜在的な脆弱性を掘り下げ、Contrast Securityがどのように対処しているかを説明する。
Go言語のマスコット「Gopher」はRenee Frenchによって作成され、CC BY 4.0( Creative Commons 4.0 Attribution)でライセンスされている。
具体的には、os.Rootは、指定された基点ディレクトリ(ルートディレクトリ)外のファイルやディレクトリにアクセスしようとするファイルシステム操作をブロックすることで動作する。この仕組みによって攻撃対象領域は大幅に縮小するものの、攻撃に対して脆弱であるかどうかのos.Rootによる判断は、多くの場合、指定されたルート内のどのファイルやフォルダが、信頼できないアクセスに対して安全であるかが把握されていることが鍵となる。
Contrastのようなセキュリティツールが、ユーザがどのファイルを非公開にしたいかを問いただすことはできないし、そうすべきではない。そのため、誤検知を最小限に抑えつつ安全性を確保するには、十分な情報に基づいた仮定を立てる必要がある。当社のContrast Assessという製品は、os.Rootを利用する開発者はパストラバーサルのリスクを認識し、それに応じてファイルシステムを適切に設定済みであるという前提で機能する。逆に、Contrast ADR(アプリケーションにおける検知と対応)は、より慎重な姿勢をとり、os.Rootのあらゆる使用を潜在的に脆弱であるものとして扱い、潜在的な脅威を軽減するために事前の対策を講じるものだ。これは、os.rootのすべての実装を本質的に危険なものとして画一的に扱っていたが、当社の以前のアプローチを改良したものである。以前のアプローチでは、この戦略が結果としてユーザにとって過剰なアラート(ノイズ)となっていた。
当社がこのような決定をした理由を説明する前に、要点を整理しよう。Goエージェントのバージョン7.3.0以降、Contrast ADRでは全てのケースでos.Rootメソッドがパストラバーサルに対して脆弱であるとみなされる。そして、Contrast Assessでは、os.Rootに安全でないルートが渡されたことが検出された場合に脆弱性として扱われる(信頼できないデータでos.Rootを初期化するとは常に脆弱性であり、そのように扱われる)。
// Assess does not register vulnerability, still guarded by ADR
os.OpenInRoot("/path/to/public/facing/dir", userInput)
// Assess detects vulnerability, guarded by ADR
os.OpenInRoot("/", userInput)
これは、Contrast AssessとADRの両方において、全てのケースでパストラバーサルに対してos.Rootメソッドを脆弱なものとして扱っていたエージェントの実装をバージョン7.2.0より緩和したものだ。Goエージェントは、v7.2.0より前のバージョンではGo1.24の新しい機能をサポートしていなかった。
Go1.24で、Go言語に新しいos.Rootという型が追加された。これは、指定された基点ディレクトリにファイルシステム操作を限定するものだ。os.Rootは、目的の基点ディレクトリをos.OpenRootに渡すことによって初期化される。それから、os.Rootメソッドで、さまざまなファイルシステム操作に対応する。
r, err :=
os.OpenRoot("./base/dir")if err != nil {
log.Fatal(err)
}
// will open the file located at ./base/dir/file
f, err := r.Open("file")
if err != nil {
log.Fatal(err)
}
// os.OpenInRoot is equivalent to the above code
f, err = os.OpenInRoot("./base/dir", "file")
if err != nil {
log.Fatal(err)
}
Goチームが書いたこのブログ記事では、なぜos.Root anを作成したのかと、どのような種類の攻撃を防ぐように設計されているかについて詳しく説明している。これらの新しいAPIによって防御層が追加されるとはいえ、悪用することは可能であり、アプリケーションが脆弱なままである可能性は依然としてある。
簡単な例から始めよう。ユーザが制御するデータを使用してos.Rootを初期化することは常に安全ではなく、アプリケーションをパストラバーサルに対して脆弱な状態にする。信頼されていないデータでos.Rootを初期化する行為自体が脆弱ではないとしても(実際は脆弱だが)、攻撃者がすべてのos.Root メソッドを、例えば/etc/で動作させていたとしたら、それがどれほど深刻な事態になるかは容易に想像できるだろう。
もっと厄介なケースは、開発者がos.Rootの対象として安全でないディレクトリを選んでしまい、これを運用者や利用者が後から適切なものに変更する手段がない場合だ。例えば、開発者がアプリケーションの基点となるディレクトリ(ルートディレクトリ)として「/」を指定してしまった場合、os.Rootによる制限はほとんど機能しない。なぜなら、ファイルシステムの最上位を指定しているので、結局のところファイルシステム全体へのアクセスが依然として許可されてしまうからだ。
他にも、もっと細かくて注意が必要なものがある。ある開発者が、次のようなディレクトリ構造を持っているとする:
userfiles
├── user1
│ └── user1.txt
└── user2
└── user2.txt
開発者が、ルートをuserfilesに設定しても、パストラバーサルに対して脆弱だ。
root, err := os.OpenRoot("./userfiles")
if err != nil {
log.Fatal(err)
}
// request logged in as user1
// assume validation of some sort checks to make sure the path starts with user1
file, err := root.Open("user1/../user2/user2_private_info.txt")
if err != nil {
log.Fatal(err)
}
data, err := io.ReadAll(file)
if err != nil {
log.Fatal(err)
}
log.Println(string(data))
このような問題をContrastで報告させるのは非常に難しい。脆弱性を特定するには、信頼できないユーザがどのファイルやフォルダにアクセスすれば安全とみなされるかを知る必要があるためだ。
要するに、os.Rootによって、指定された基点ディレクトリ外へのトラバーサルは防げるが、基点ディレクトリ内の意図しないファイルへのアクセスを防ぐことはできない。
このため、Goエージェントのバージョン7.2.0で最初にos.Rootに対応した際、当社では「os.Rootが使われている箇所は全て脆弱である可能性がある」という前提でいたのだが、この考え方では誤検知が多発生するのではないかという懸念があった。結局のところ、そもそもos.RootのAPIを使用する開発者であれば、パストラバーサルの脆弱性がどのようなものであるかを理解していて、適切な対策を講じているはずだと想定している。つまり、ほとんどの場合、os.Root は安全に使われている可能性が高いと考えている。
当社では過検知を避けることで、ノイズを最小限に抑えることを常に目指している。つまり、基点ディレクトリにユーザがアクセスしても安全なデータしか含まれていない場合、パストラバーサルに対して脆弱であるとは報告したくないということだ。残念ながら、これは実際に把握できるものではない。
現時点での当社の方針としては、こうだ。もしos.Rootを使用している場合は、パストラバーサルへの対策方法が認識されており、ルートディレクトリを過度に広範な場所には設定していないものと判断する。ただし、ルートディレクトリとして「 /」や「/etc」のような、特に注意が必要なパスが指定されている場合は、引き続き監視を行うことにしている。
攻撃から防御する際は、判断基準が異なる。Contrast ADRのブロックモードで何らかの入力が疑わしいと検知された場合、それは攻撃の試みである可能性が非常に高いものであると見なされる。そのため、例えそれが必ずしもグロックする必要のない入力であったとしても、疑わしいものはブロックしてしまう方が、はるかに許容できる対応策なのだ。
payload := "../../../../../../../etc/passwd"
os.OpenInRoot("./safe/dir", payload)
Contrastでは、関数が実行される前に常にブロックされることに留意してほしい(そうでない場合、すでに脆弱性が悪用されている可能性がある)。 つまり、ブロックモードで上記のコードがGoエージェントと一緒に実行されると、os.Rootエラーが返る前にGoエージェントで攻撃がブロックされるということだ。ADRの監視モードが有効になっている場合は、例えos.Rootによってパストラバーサルが阻止されたとしても、エージェントから脆弱性の悪用が報告される。これは、os.Rootで攻撃を許しているという意味ではない。これが意味するのは、Contrastでos.Rootメソッドのコードへの攻撃を検知はしたが、阻止はしていないことを意味する(Contarstで監視モードの場合、これは正しい動き)。このような動きにより、通常よりも多くのノイズが発生する可能性があることは認識しているが、本番環境で起こりうる攻撃をブロックする際には注意を怠らないことが重要だ。
要するに、os.Rootメソッドへの攻撃が報告されたからといって、その攻撃がos.Rootの基準ディレクトリを通過したことを意味するわけではない。そのようなことは、あってはならない。これは、開発者がos.Rootに安全な場所が指定されていることの確認が必要であるということだけのことだ。
まとめると、新しいos.Root型の主な利点と、攻撃の影響を受ける可能性がある例は以下のようになる。この両方の側面を理解することは、効果的なセキュリティ実装のために非常に重要だ。
最後までお読み頂きありがとう。主なポイントは以下の通り:
質問や不明な点があれば、Contrastのサポート担当までお気軽にお問い合わせを。
私たちのブログを購読することで、最新のappsecニュースをすべて把握し、ベストプラクティスを開発できます。また、最新のコントラスト製品ニュースとエキサイティングなアプリケーションセキュリティイベントについても通知されます。