C/C++の外部入力による書式文字列の制御

信頼できない入力をprintf系関数の書式文字列に使うリスク

説明

printf 系の関数は、書式引数のパーセント記号で始まる指定子を解釈し、後続の引数から対応する値を読み取ります。信頼できない入力を書式として渡すと、攻撃者が %x、%s、位置指定の引数などを注入できます。その結果、プロセスのメモリを読み取られたり、不正な引数へのアクセスでプロセスが停止したりするおそれがあります。%n は、それまでに出力した文字数を引数が指すメモリへ書き込むため、メモリの改ざんにつながる場合もあります。

snprintf() で出力長を制限しても、書式文字列自体は安全になりません。POSIXの syslog() も printf と同様にメッセージを整形するため、メッセージ引数に同じ対策が必要です。

想定される影響

  • プロセスのメモリやアドレスなどの機密情報の漏えい
  • 不正なメモリアクセスや過剰な出力によるプロセスの停止
  • 実行環境や後続の引数によっては、メモリの改ざんやプロセスの権限でのコード実行

対処方法

  • 書式文字列をコード内のリテラルに固定し、信頼できないテキストは %s などに対応する通常の値引数として渡します。
  • バッファーへの出力関数、va_list を使う関数、システムログにも同じ原則を適用します。
  • C++23に準拠した実装では、std::print("{}", input) でリテラルの書式を使ってテキストを出力できます。ストリーム出力が適する場合は、std::cout << input もパーセント指定子を解釈しません。
  • sanitize_format() という関数名や % の置換だけで安全と判断しないでください。固定の書式を使い、引数の数と型も指定子に合わせます。
  • GCCの -Wformat=2 などの書式診断を有効にし、ミスを検出します。コンパイラーの警告は、書式の固定に代わる対策ではありません。

例

標準出力

変更前

c
#include <stdio.h>

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

    printf(argv[1]);
    return 0;
}

argv[1] のパーセント指定子が書式構文として解釈されます。

変更後

c
#include <stdio.h>

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

    printf("%s", argv[1]);
    return 0;
}

書式をリテラルに固定し、argv[1] は文字列の値としてだけ扱います。

C++の std::printf()

cpp
#include <cstdio>

void show_user_input(const char *input) {
    // 変更前: inputが書式文字列として解釈されます。
    std::printf(input);

    // 変更後: inputは%sに対応する値引数です。
    std::printf("%s", input);
}

std:: 名前空間を使っても、C形式の書式文字列の意味は変わりません。C++23の std::print() でも、std::print("{}", input) のように書式をリテラルに保ちます。

システムログ

c
#include <syslog.h>

void log_user_input(const char *input) {
    /* 変更前: inputが書式文字列として解釈されます。 */
    syslog(LOG_NOTICE, input);

    /* 変更後: inputは%sに対応する値引数です。 */
    syslog(LOG_NOTICE, "%s", input);
}

参考資料