In this article
GoアプリケーションでSSRFの脆弱性を回避する方法
Goは、スケーラブルで効率的なWebアプリケーションの構築において、疑いなく人気が高まり続けています。そのため、SSRFなどのセキュリティ脆弱性への対処が不可欠です。Goの並行処理モデルとパフォーマンスは、サーバーサイドアプリケーションに最適ですが、開発者は潜在的なセキュリティ脅威に常に警戒する必要があります。
この記事では、GoアプリケーションでSSRFの脆弱性がどのように発生するかを解説し、開発者がアプリケーションとデータを保護するために効果的なセキュリティ対策を実装する方法を紹介します。
コードを段階的に分析してSSRFの脆弱性を特定し、それに伴うセキュリティリスクについて説明します。また、Snyk Codeなどのツールを紹介します。これらのツールを使えば、アプリケーションを本番環境にデプロイする前に、開発者がSSRFの脆弱性を検出して修正できます。
SSRF脆弱性とは何ですか?
サーバーサイドリクエストフォージェリ(SSRF)は、攻撃者がサーバーサイドアプリケーションから意図しない宛先へリクエストを送信できる、重大なセキュリティ脆弱性です。これにより、内部サービスへの不正アクセスや機密データの漏えい、さらにはリモートコード実行につながるおそれがあります。SSRFは、サーバーが内部ネットワークの通信を信頼していることを悪用し、攻撃者がURLを操作してリクエストを悪意のあるエンドポイントに転送できるようにします。
SSRFの一般的な攻撃経路と影響
SSRFの脆弱性は、アプリケーションがユーザー入力を受け取り、適切な検証やサニタイズを行わずにサーバーサイドリクエスト用のURLを生成すると発生することがよくあります。攻撃者は悪意ある入力を仕込み、サーバーのリクエストを意図しない宛先にリダイレクトさせます。宛先には次のようなものがあります。
内部サービス(クラウド環境のメタデータサービスなど)
ローカルホストのインターフェース
その他の内部ネットワークリソース
攻撃者がGoアプリケーションのSSRF脆弱性を悪用した場合、次のような深刻な影響が生じる可能性があります。
データの窃取:脆弱なGoアプリケーションからアクセスできる他のマイクロサービスなど、内部サービスから機密データにアクセスする。
サービスの妨害:内部サービスに過剰な負荷をかけたり、サービスを妨害したりするリクエストを送信する。
脆弱性のあるGoアプリケーション
以下のGoアプリケーションは、Gin Webフレームワークを使って構築したシンプルなHTTPサーバーです。画像ファイルをダウンロードしてサイズを変更できるPOSTエンドポイント/cloudpawnery/imageを公開しています。主な構成要素を見ていきましょう。
設定とGoの構造体:
Webプログラムの以下のGoコードでは、リモートからファイルを取得する機能の初期設定と、APIルートへのリクエストで想定されるデータスキーマを定義します。
const baseHost = "localtest.me:8080"
type FileInfo struct {
Filename string `json:"filename"`
Download string `json:"download"`
}上記のコードでは、次のようになっています。
baseHostは、ファイルをダウンロードするURLの生成に使用するベースドメインを定義する定数です。
FileInfoは、ファイルストレージサーバーから返されるJSON構造を表す構造体です。
HTTP経由でアセットをダウンロードしてサイズを変更する関数
downloadAndResize関数は、tenantID、fileID、fileSizeをパラメーターとして受け取ります。ダウンロードしてサイズを変更するファイルの情報を含むJSONファイルを取得するためのURLを生成します。
func downloadAndResize(tenantID, fileID, fileSize string) error {
// Example input based on the mocked storage server in fixtures/http directory of the project:
log.Printf("Processing request for tenantID: %s, fileID: %s", tenantID, fileID)
urlStr := fmt.Sprintf("http://%s.%s/storage/%s.json", tenantID, baseHost, fileID)
fmt.Println("Resolved URL: ", urlStr)
// Parse the URL to extract the hostname
parsedURL, err := url.Parse(urlStr)
if (err != nil) {
panic(err)
}
fmt.Println("Resolved Hostname: ", parsedURL.Hostname())
// Make HTTP request
resp, err := http.Get(urlStr)
if (err != nil) {
panic(err)
}
defer resp.Body.Close()
// Read response body
body, err := ioutil.ReadAll(resp.Body)
if (err != nil) {
panic(err)
}
// Decode JSON data
var info FileInfo
err = json.Unmarshal(body, &info)
if (err != nil) {
panic(err)
}
// Download file
downloadResp, err := http.Get(info.Download)
if (err != nil) {
panic(err)
}
defer downloadResp.Body.Close()
// Create target filename
targetFilename := fmt.Sprintf("uploads/%s", info.Filename)
// read the downloaded file into memory
fileBytes, err := ioutil.ReadAll(downloadResp.Body)
if (err != nil) {
panic(err)
}
// Save downloaded file
err = ioutil.WriteFile(targetFilename, fileBytes, 0644)
if (err != nil) {
panic(err)
}
convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)
fmt.Println("Running command:", convertCmd)
_, err = exec.Command("sh", "-c", convertCmd).Output()
if (err != nil) {
fmt.Println("Error resizing image:", err)
} else {
fmt.Println("Downloaded and resized image:", targetFilename)
}
return nil
}この関数は次の処理を実行します。
tenantIDとfileIDを使ってURLを生成する。HTTP GETリクエストを送信してJSONデータを取得する。
JSONを解析し、ダウンロードURLとファイル名を抽出する。
ファイルをダウンロードしてローカルに保存する。
convertコマンドを使って画像のサイズを変更する。
Goのメイン関数
Goプログラムの中心となる部分では、Webサーバーをインスタンス化してPOSTエンドポイントのルートを定義し、downloadAndResize関数を呼び出します。
func main() {
// Create a Gin router
router := gin.Default()
// Define a POST endpoint
router.POST("/cloudpawnery/image", func(c *gin.Context) {
// If data lives on the query string we can use this:
tenantID := c.Query("tenantID")
fileID := c.Query("fileID")
fileSize := c.Query("fileSize")
if (fileSize == "") {
fileSize = "200"
}
// Validate tenantID and fileID
if (tenantID == "" || fileID == "") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing tenantID or fileID"})
return
}
// Call the download and resize function
err := downloadAndResize(tenantID, fileID, fileSize)
if (err != nil) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return a success response
c.JSON(http.StatusOK, gin.H{"message": "File downloaded and resized successfully"})
})
// Start the HTTP server
router.Run(":7000")
}POSTエンドポイントは、HTTPルート/cloudpawnery/imageで定義されています。クエリパラメーターからtenantID、fileID、fileSizeを取得し、さらに次の処理を行います。
- tenantIDとfileIDが指定されていることを検証する。
- downloadAndResizeを呼び出して画像を処理する。
- 成功または失敗を示すJSONレスポンスを返す。
コード内のSSRF脆弱性を特定する
このGoアプリケーションのSSRF脆弱性は、tenantIDをHTTPリクエスト用のURLの生成に使用する方法に起因します。
セキュリティ脆弱性について、詳しく分析してみましょう。
ユーザーが制御する入力:
tenantIDは、検証やサニタイズを行わずにHTTPリクエストのクエリ文字列から直接取得されます。そのため、攻撃者はtenantIDを操作して、任意のドメインを指定できます。
URLの生成:downloadAndResize関数は、tenantIDとfileIDを使ってURLを生成します。ここで、この文字列連結がURLの安全な生成方法かどうか考えてみてください。どのような問題が起こり得るでしょうか。
urlStr := fmt.Sprintf("http://%s.%s/storage/%s.json", tenantID, baseHost, fileID)攻撃者が悪意あるtenantIDを指定すると、サブドメイン全体を制御し、リクエストを悪意あるサーバーにリダイレクトできる可能性があります。
HTTPリクエスト:アプリケーションは、生成されたURLにHTTP GETリクエストを送信します。
resp, err := http.Get(urlStr)このリクエストは攻撃者が制御するサーバーにリダイレクトされる可能性があり、攻撃者は内部サービスへのアクセスや、その他の悪意ある操作を行えるようになります。
SSRFにつながる脆弱なGoコード
このGoアプリケーションでは、HTTPリクエストのクエリ文字列からtenantIDを取得し、URLの生成に直接使用しています。そのURLは、検証やサニタイズを行わずにhttp.Get関数によるGETリクエストに使われます。
この実装は、サーバーサイドリクエストフォージェリ(SSRF)の脆弱性につながります。攻撃者はtenantIDを操作して、意図しないホストを指すURLを生成し、機密性の高い内部サービスやデータにアクセスする可能性があります。
悪意あるtenantIDを使ったSSRF脆弱性の悪用
攻撃者は悪意あるtenantIDを指定し、リクエストを意図しないサーバーにリダイレクトすることで、この脆弱性を悪用できます。
たとえば、攻撃者がtenantIDにe:@evil.com/url=のような値を指定してリクエストを送信すると、生成されるURLはhttp://1234:e@evil.com/url=.localtest.me:8080/storage/fileID.jsonになります。このURLはローカルのマイクロサービスではなくevil.comからJSON情報を取得するため、攻撃者はストレージファイルの設定を操作できるようになります。
Snyk Codeを活用したSSRFの検出とセキュリティ分析
Goのコードに潜むこのSSRF脆弱性をどうやって見つけたのか、気になりませんか?
VS Code IDEにSnyk拡張機能をインストールしています。コードを保存すると赤い波線が表示され、セキュリティ脆弱性につながる可能性のある安全でないコード行を追加したことをSnykが検知します。
Snykがサーバーサイドリクエストフォージェリの脆弱性をリアルタイムで検出し、警告する仕組みを紹介します。

ご覧のとおり、Snykはセキュリティ脆弱性を検出するだけでなく、ユーザーが制御する入力がHTTP呼び出しという形式でシンク関数に到達するまでの、詳細なコードパスと呼び出しフローも表示します。
Snykはさらに一歩進んで、他のオープンソースプロジェクトのコミットをもとに、セキュリティ問題を緩和するための修正案も複数提示します。
同じ体験を通じてコードを保護するには、Snyk拡張機能をインストールするか、GitHub、GitLab、BitbucketのコードリポジトリをSnykアプリに接続してください。継続的に監視し、脆弱性の検出と修正を行います。
Goのセキュリティについてさらに学ぶために、Go開発者の皆さんにはGoのセキュリティに関するベストプラクティスを読むことをおすすめします。