説明
system()、popen()、_popen() は、文字列をコマンドインタープリターに渡します。C++で <cstdlib> の std::system() を呼ぶ場合も同様です。信頼できない入力が文字列に含まれると、シェルのメタ文字、コマンドの区切り、リダイレクト、追加の引数などがコードとして解釈される可能性があります。wordexp() もシェルの単語展開を行い、WRDE_NOCMD を指定しない場合はコマンド置換を実行できます。
シェルの特殊文字をエスケープするだけでは、すべてのシェルや実行先のプログラムの構文を安全に扱えません。シェルコマンドを防いでも、実行先が信頼できない値をオプションとして解釈すると、引数インジェクションが残る場合があります。shell_escape() や sanitize_command_arg() といった関数名だけで安全性を判断しないでください。
想定される影響
- アプリケーションの権限で任意のOSコマンドを実行されるおそれがあります。
- ファイルの削除・改ざん、機密情報の漏えい、内部システムへのアクセス、サービス停止につながる可能性があります。
- 高い権限で動作するプロセスでは、システム全体を侵害されるおそれがあります。
対処方法
可能なら外部コマンドを呼ばず、必要な機能を提供するライブラリAPIを使います。外部プロセスが必要な場合は、次の対策を組み合わせます。
- 実行ファイルのパスをコード内で固定し、
execve()、適切なposix_spawn()関数、またはプラットフォームのプロセスAPIを使います。 - コマンド文字列を組み立てず、実行ファイルと引数を分けます。
- 実行先が期待するデータ形式の許可リストで各引数を検証します。先頭の
-や--の扱いなど、オプションを注入される可能性も確認します。 - 必要な環境変数だけに信頼できる値を設定し、エラーを処理します。
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の環境で有効です。許可リストは、値を業務上必要な形式にも制限します。
参考資料
- POSIX.1-2024
system() - POSIX.1-2024 シェルコマンド言語
- POSIX.1-2024
wordexp() - POSIX.1-2024
exec関数群 - POSIX.1-2024
posix_spawn() - Microsoft
_popen、_wpopen - Microsoft C++標準ライブラリ
<cstdlib> - Microsoft
system、_wsystem - ISO/IEC 14882:2024 プログラミング言語 — C++
- OWASP OSコマンドインジェクション防御チートシート
- OWASP ASVS 5.0.0 V1.2.5
- SEI CERT C ENV33-C:
system()を呼び出さない - CWE-78: OSコマンド内の特殊要素の不適切な無害化
- OWASP Top 10:2025 A05 インジェクション
- OWASP Top 10:2021 A03 インジェクション