OSコマンドインジェクション

C/C++のOSコマンドインジェクション

説明

system()、popen()、_popen() は、文字列をコマンドインタープリターに渡します。C++で <cstdlib> の std::system() を呼ぶ場合も同様です。信頼できない入力が文字列に含まれると、シェルのメタ文字、コマンドの区切り、リダイレクト、追加の引数などがコードとして解釈される可能性があります。wordexp() もシェルの単語展開を行い、WRDE_NOCMD を指定しない場合はコマンド置換を実行できます。

シェルの特殊文字をエスケープするだけでは、すべてのシェルや実行先のプログラムの構文を安全に扱えません。シェルコマンドを防いでも、実行先が信頼できない値をオプションとして解釈すると、引数インジェクションが残る場合があります。shell_escape() や sanitize_command_arg() といった関数名だけで安全性を判断しないでください。

想定される影響

  • アプリケーションの権限で任意のOSコマンドを実行されるおそれがあります。
  • ファイルの削除・改ざん、機密情報の漏えい、内部システムへのアクセス、サービス停止につながる可能性があります。
  • 高い権限で動作するプロセスでは、システム全体を侵害されるおそれがあります。

対処方法

可能なら外部コマンドを呼ばず、必要な機能を提供するライブラリAPIを使います。外部プロセスが必要な場合は、次の対策を組み合わせます。

  1. 実行ファイルのパスをコード内で固定し、execve()、適切な posix_spawn() 関数、またはプラットフォームのプロセスAPIを使います。
  2. コマンド文字列を組み立てず、実行ファイルと引数を分けます。
  3. 実行先が期待するデータ形式の許可リストで各引数を検証します。先頭の - や -- の扱いなど、オプションを注入される可能性も確認します。
  4. 必要な環境変数だけに信頼できる値を設定し、エラーを処理します。
  5. wordexp() が避けられない場合は、WRDE_NOCMD を含めて戻り値を確認します。このフラグはコマンド置換の要求を WRDE_CMDSUB エラーで拒否しますが、ほかの単語展開全般を安全にするものではありません。

例

C

変更前

c
#include <stdlib.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        return 2;
    }

    return system(argv[1]);
}

変更後

c
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static int valid_label(const char *value) {
    size_t length = strlen(value);
    if (length == 0 || length > 64) {
        return 0;
    }

    for (size_t i = 0; i < length; ++i) {
        unsigned char ch = (unsigned char)value[i];
        if (!isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
            return 0;
        }
    }
    return 1;
}

int main(int argc, char **argv) {
    if (argc != 2 || !valid_label(argv[1])) {
        return 2;
    }

    char *const args[] = {"printf", "Archive: %s\n", argv[1], NULL};
    execv("/usr/bin/printf", args);
    perror("execv");
    return 1;
}

説明:

  • 変更前: argv[1] 全体がシェルで解釈されるコマンドになります。引用符を加えても、すべてのシェル構文を安全にはできません。
  • 変更後: 実行ファイルと書式文字列を固定し、ユーザーの値を別の引数として渡します。許可リストは、値を業務上必要な形式にも制限します。

wordexp() が必要な場合は、コマンド置換を明示的に禁止し、すべてのエラーを処理します。

c
#include <wordexp.h>

int expand_without_commands(const char *input, wordexp_t *words) {
    int result = wordexp(input, words, WRDE_NOCMD | WRDE_UNDEF);
    if (result != 0) {
        if (result == WRDE_NOSPACE) {
            wordfree(words);
        }
        return -1;
    }
    return 0;
}

成功時は呼び出し元が結果を使い終わった後に wordfree() で解放します。WRDE_NOSPACE エラー時も、部分的に確保されたメモリの解放が必要です。

C++

変更前

cpp
#include <cstdlib>

int main(int argc, char **argv) {
    if (argc != 2) {
        return 2;
    }

    return std::system(argv[1]);
}

変更後

cpp
#include <cctype>
#include <cstdio>
#include <cstring>
#include <unistd.h>

static bool valid_label(const char *value) {
    std::size_t length = std::strlen(value);
    if (length == 0 || length > 64) {
        return false;
    }

    for (std::size_t i = 0; i < length; ++i) {
        unsigned char ch = static_cast<unsigned char>(value[i]);
        if (!std::isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
            return false;
        }
    }
    return true;
}

int main(int argc, char **argv) {
    if (argc != 2 || !valid_label(argv[1])) {
        return 2;
    }

    char program[] = "/usr/bin/printf";
    char format[] = "Archive: %s\n";
    char *const args[] = {program, format, argv[1], nullptr};
    execv(program, args);
    std::perror("execv");
    return 1;
}

std::system() の代わりに固定の実行ファイルを直接呼び出し、ユーザーの値を別の引数として渡します。この例はC++23とPOSIX.1-2024の環境で有効です。許可リストは、値を業務上必要な形式にも制限します。

参考資料