Skip to main content

Goにおけるコマンドインジェクションの脆弱性を理解する

著者
feature insights context

2024年11月14日

0 分で読めます

Goの開発者は、画像の処理やサイズ変更などのさまざまな場面で、システムコマンドを使う必要があるかもしれません。また、リソースの管理やメトリクス、ログの収集にシステムコマンドを実行することもあります。

また、既存のレガシーシステムと連携する新しいシステムをGoで構築している場合もあるでしょう。この連携では、システムコマンドを実行し、その出力を処理する方法が用いられます。

いずれの場合も、システムコマンドを起動する際には、安全なコーディング規約に従うことが不可欠です。そうしないと、コマンドインジェクションと呼ばれるセキュリティ脆弱性につながるおそれがあります。

コマンドインジェクションとは?

コマンドインジェクションとは、アプリケーションが安全でないユーザー提供データ(Webフォームからの入力など)をシステムシェルに渡すことで発生するセキュリティ脆弱性です。この弱点を悪用すると、攻撃者はアプリケーションと同じユーザー権限で、ホストOS上の任意のコマンドを実行できます。

Goでは、コマンドインジェクションは多くの場合、os/execパッケージを使用してシステムコマンドを起動する際に発生します。

次のGoのコード例を見てみましょう。

func handler(req *http.Request) {
  cmdName := req.URL.Query()["cmd"][0]
  cmd := exec.Command(cmdName)
  cmd.Run()
}

このコードでは、cmdNameをリクエストのクエリパラメーターから直接取得し、その値を使ってサーバー上で実行するコマンドを構成しています。これは典型的なコマンドインジェクションの脆弱性です。攻撃者はcmdクエリ文字列の値を改ざんして、任意のコマンドを実行できるため、サーバーが侵害されるおそれがあります。

攻撃者は、次のような悪意のあるコマンドを含むリクエストを作成できます。

http://example.com/execute?cmd=rm%20-rf%20/

コマンドインジェクションはなぜ危険なのでしょうか?攻撃者がサーバー上で任意のコマンドを実行できるため、次のような深刻な結果につながるおそれがあります。

  • データ侵害:攻撃者はサーバーに保存された機密データにアクセスできます。リソースや認証情報が記載されたconfig.tomlなどの設定ファイルにアクセスされる事態を想像してみてください。

  • システムの侵害:攻撃者はサーバーを制御し、さらなる攻撃を仕掛けることができます。侵害されたシステム内で横方向に移動したり、侵害可能な別のサーバーを探したりするケースがよく見られます。

  • サービスの中断:攻撃者はサービスを妨害するコマンドを実行し、ダウンタイムを引き起こせます。サービス拒否攻撃による損失は、直接的な金銭的損害やビジネスリスクにつながる可能性があります。

コマンドインジェクション攻撃が成功した場合、その影響は甚大です。侵害されたシステムだけでなく、組織の評判や顧客からの信頼にも悪影響を及ぼします。

Goにおける脆弱なプロセス実行とコマンドインジェクション

シンプルさとパフォーマンスを重視するGoの開発者は、ユーティリティの機能を活用するために、システムコマンドを統合することがあります。これにより、機能を一から作り直さずに、堅牢なアプリケーションの開発に集中できます。しかし、この統合には、特にセキュリティ面で独自の課題があります。

より現実的な例として、convertのようなコマンドラインユーティリティを使って画像ファイルを処理するGoアプリケーションを考えてみましょう。

画像のサイズ変更リクエストを処理するGoアプリケーションを見てみましょう。このアプリケーションではGin Webフレームワークを使用してPOSTエンドポイント/cloudpawnery/imageを定義し、ユーザー入力に基づいて画像のサイズを変更します。このエンドポイントは、クエリ文字列からtenantID、fileID、fileSizeなどのパラメーターを受け取ります。fileSizeパラメーターは省略可能で、指定されない場合は「200」がデフォルト値になります。

次のコードは、Goでの脆弱な実装例です。

func main() {
    // Create a Gin router
    router := gin.Default()

    // Define a POST endpoint
    router.POST("/cloudpawnery/image", func(c *gin.Context) {
        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(c, 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")
}

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    slog.Info("Processing request", "tenantID", tenantID, "fileID", fileID)

    convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)
    slog.Info("Running command", "command", convertCmd)

    _, err := exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()
    if err != nil {
        slog.Error("Error resizing image", "error", err)
        return fmt.Errorf("%w: %v", ErrImageResize, err)
    }

    slog.Info("Downloaded and resized image", "filename", targetFilename)
    return nil
}

downloadAndResize関数は、convertを使って画像のサイズを変更するコマンド文字列を作成し、exec.CommandContextで実行します。

downloadAndResize関数は、どのようにコマンド文字列を作成するのでしょうか?ユーザーが指定したfileSizeの値を入力として受け取ります。この文字列が実行されるため、攻撃者が悪意のあるコマンドを注入できる可能性があります。このリスクを軽減するには、Goの開発者はすべてのユーザー入力を検証・サニタイズし、パラメーター化されたコマンドを使用して、コマンド実行を安全に処理する手法を活用する必要があります。

コマンドインジェクションの脆弱性を軽減する

Goの開発でも、ほかの言語と同様に、コマンドインジェクションの脆弱性からコードを守ることが極めて重要です。コマンドインジェクションは、攻撃者がホスト上で任意のコマンドを実行できる状態を指し、不正アクセスやデータ侵害など、深刻なセキュリティ問題につながるおそれがあります。

こうしたリスクを軽減するためのベストプラクティスをいくつか見てみましょう。

入力の検証とサニタイズ

コマンドインジェクションを防ぐ基本的な対策の一つが、入力を厳格に検証してサニタイズすることです。先ほどの例では、downloadAndResize関数がfileSizeなどのユーザー入力を使ってコマンド文字列を作成します。これらの入力が適切に検証されていないと、攻撃者が悪意のあるコマンドを注入するおそれがあります。

入力の検証を強化する方法は次のとおりです。

  1. 許可リストを使う:fileSizeなどの入力に対して、許容する値を定義します。たとえば、妥当な範囲内の数値だけを許可します。

  2. 入力をサニタイズする:ユーザー入力から、危険な可能性のある文字を削除またはエスケープします。これには;、|、&などのシェルのメタ文字も含まれます。コマンドはハードコードし、ユーザー入力によって制御されないようにするのが最善です。

  3. 厳密な型を使う:入力はできるだけ早く、想定されるデータ型に変換します。たとえば、fileSizeを整数に変換し、その範囲を検証します。

これらの対策を実装する例を見てみましょう。

func sanitizeInput(input string) (int, error) {
    size, err := strconv.Atoi(input)
    if err != nil || size <= 0 || size > 1000 {
        return 0, fmt.Errorf("invalid input")
    }
    return size, nil
}

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    size, err := sanitizeInput(fileSize)
    if err != nil {
        return fmt.Errorf("invalid file size: %w", err)
    }

    convertCmd := fmt.Sprintf("convert %s -resize %dx%d %s", targetFilename, size, size, targetFilename)
    // ...
}

それでも、コマンドインジェクションからGoのコードを守るためにできることは、まだたくさんあります。続きをご覧ください。

システムコマンドの代わりに安全なAPIやライブラリを使う

もう一つの効果的な対策は、可能な限りシステムコマンドを直接実行しないことです。代わりに、コマンドインジェクションのリスクにアプリケーションをさらすことなく、必要な機能を提供する安全なAPIやライブラリを活用しましょう。

たとえば、アプリケーションで画像を操作する必要がある場合は、ImageMagickの外部コマンドconvertを呼び出す代わりに、github.com/disintegration/imagingのようなGoライブラリを検討してください。Goの型安全な環境内で機能を処理できるため、攻撃対象領域を小さくできます。

import (
    "github.com/disintegration/imaging"
    // ...
)

func resizeImage(inputPath, outputPath string, width, height int) error {
    img, err := imaging.Open(inputPath)
    if err != nil {
        return fmt.Errorf("failed to open image: %w", err)
    }

    resizedImg := imaging.Resize(img, width, height, imaging.Lanczos)
    err = imaging.Save(resizedImg, outputPath)
    if err != nil {
        return fmt.Errorf("failed to save image: %w", err)
    }

    return nil
}

Goでimagingのようなライブラリを使えば、シェルコマンドを作成して実行する必要がなくなり、コマンドインジェクションのリスクを軽減できます。ただし、ライブラリや言語エコシステムによっては、サードパーティーパッケージがコマンド実行の単純なラッパーにすぎない場合もあります。そのため、こうした機密性の高い処理については、コードを確認することが必須です。

脆弱なdownloadAndResize関数をリファクタリングしてインジェクションを防ぐ

先ほどの例では、exec.CommandContext関数を使ってシェルコマンドを実行する、脆弱性のある可能性が高いGoアプリケーションを紹介しました。先述のとおり、この方法はコマンドインジェクションの脆弱性につながるおそれがあります。

ユーザー入力によって任意のコマンドが実行されないように、downloadAndResize関数をリファクタリングしてみましょう。

コマンドインジェクションを防ぐ効果的な方法の一つは、ユーザー入力からシェルコマンド文字列を直接作成しないことです。代わりに、exec.Command関数と個別の引数を使えば、シェルを呼び出さずにユーザー入力をコマンドのパラメーターとして安全に渡せます。また、ユーザーがコマンド自体を制御できないようにできます。

コマンドインジェクションの脆弱性に対処した、downloadAndResize関数のリファクタリング例を紹介します。

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    slog.Info("Processing request", "tenantID", tenantID, "fileID", fileID)

    // Define the command and its arguments separately
    args := []string{targetFilename, "-resize", fmt.Sprintf("%sx%s", fileSize, fileSize), targetFilename}
    cmd := exec.CommandContext(ctx, "convert", args...)

    slog.Info("Running command", "command", cmd.String())

    // Execute the command
    _, err := cmd.CombinedOutput()
    if err != nil {
        slog.Error("Error resizing image", "error", err)
        return fmt.Errorf("%w: %v", ErrImageResize, err)
    }

    slog.Info("Downloaded and resized image", "filename", targetFilename)
    return nil
}

このリファクタリングでは、コマンドと引数を分けました。exec.CommandContextに個別の引数を渡すことで、シェルコマンド文字列を作成する必要がなくなります。これにより、ユーザー入力は実行可能なコードではなくデータとして扱われるため、コマンドインジェクションのリスクを大幅に軽減できます。

また、シェルを呼び出す必要もなくしました。リファクタリング後のコードは、コマンドインジェクションの一般的な経路であるシェル(sh -c)を呼び出しません。代わりに、指定した引数を使ってconvertユーティリティを直接実行します。

コードセキュリティにSnykを活用する

Snyk Codeは、開発者がコードベースの脆弱性を特定して修正できる強力な静的解析ツールです。IDEや開発ワークフローにシームレスに統合され、潜在的なセキュリティ問題をリアルタイムで通知します。

SnykがGoのコマンドインジェクションの脆弱性を特定する方法

先ほどのGoの例では、downloadAndResize関数がユーザー入力を使ってシェルコマンドを作成します。

convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)

_, err := exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()

このコードは、ユーザー入力をコマンド文字列に直接組み込んでいるため、コマンドインジェクションに対して脆弱です。

チームの開発者がコマンドインジェクションの脆弱性を知らなかったら、どうなるでしょうか?

コードレビューでファイル間の静的解析の呼び出しフローを簡単に追跡し、このコマンドインジェクションの脆弱性を見つけられるでしょうか?

そんなときに役立つのがSnykです。

VS Codeエディターで脆弱なコードをリアルタイムにハイライトするSnyk Code拡張機能を使えば、アプリケーションセキュリティを簡単に実現できます。

コマンドインジェクションの脆弱性があるGoコードと、修正後のexec.Commandの例を示すSnyk Codeの画面。

SnykはGoコードをスキャンし、シェルコマンドでユーザー入力が安全でない方法で使われている箇所を検出して、脆弱性の特定を支援します。また、この脆弱性を修正したオープンソースプロジェクトの実際のコミットを提示するため、「適切なコード」の参考例を確認できます。

さらに、ISSUE OVERVIEWタブをクリックすると、コマンドインジェクションの防止に関する詳しい情報やベストプラクティスを確認できます。このタブでは、リスクを軽減するための詳細な分析と推奨事項を、IDE上でコーディングしながら参照できます。高コストなコンテキストスイッチも必要ありません。

Understanding Command Injection vulnerabilities in Go

こうした脆弱性からコードを守る方法をさらに学ぶには、Snyk CodeのIDE拡張機能をインストールし、Gitプロジェクトを接続して、Goアプリケーションのセキュリティ問題を特定・修正しましょう。登録すれば、コードの脆弱性スキャンを簡単かつ無料で始められます。

無料のコードチェッカーツール

次のコミットの前にコードを安全にしましょう。