🇯🇵 日本語 | 🇺🇸 English
広告スペース

C言語 安全な文字列操作

危険な関数と安全な代替を並べて覚える。バッファオーバーフローは過去数十年のセキュリティ脆弱性の上位。

なぜ危険なのか(バッファオーバーフロー)

バッファオーバーフローは配列の領域を超えてメモリに書き込んでしまうバグです。隣接するスタック領域を壊してプログラムがクラッシュするだけでなく、悪意のある入力で任意コード実行まで許してしまう深刻な脆弱性です。
#include <string.h>

int main(void) {
    char buf[8];                      // 8バイトしかない
    strcpy(buf, "This is too long!"); // 18バイト書き込み → オーバーフロー
    return 0;
}
結果: スタック上の戻りアドレスなどを破壊し、Segmentation Fault か、最悪の場合は任意コード実行。C言語では関数が長さをチェックしないので、呼び出し側が安全を保証する必要がある。

覚えるべき原則

  1. バッファのサイズを必ず明示的に関数に渡す(sizeof / 定数)
  2. n系 / 範囲を取る関数を優先する(strncpy, snprintf, fgets, strtol)
  3. 戻り値をチェックして切り詰めやエラーを検出する
  4. 最後にNUL終端を確認する

strcpy → strncpy / snprintf

❌ strcpy (危険)

char dst[8];
strcpy(dst, src);
// src の長さを dst がチェックしない
// src が8バイト以上ならオーバーフロー

✅ strncpy + 終端保証

char dst[8];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';
// 最後に必ず'\0'を入れる
strncpy の落とし穴: コピーしたバイト数が n に達すると '\0' を付けない。必ず末尾に dst[size-1] = '\0'; を明示的に入れること。

snprintf を使うのが最も無難

char dst[8];
int n = snprintf(dst, sizeof(dst), "%s", src);
// snprintf は常に'\0'終端する (size>0 のとき)
// 戻り値 n は「書きたかったバイト数」。n >= sizeof(dst) なら切り詰め発生
if (n >= (int)sizeof(dst)) {
    fprintf(stderr, "warning: truncated\n");
}
推奨順位: snprintf > strncpy+NUL終端 >> strcpy。他人が読んだときに安全と分かる書き方を選ぼう。

sprintf → snprintf

❌ sprintf

char buf[32];
sprintf(buf, "Hello, %s!", name);
// name が長いと buf を越える

✅ snprintf

char buf[32];
snprintf(buf, sizeof(buf), "Hello, %s!", name);
// buf のサイズで切り詰める

戻り値で切り詰めを検知

int n = snprintf(buf, sizeof(buf), fmt, ...);
if (n < 0)                 { /* エンコードエラー */ }
else if (n >= sizeof(buf)) { /* 切り詰め発生 */ }
else                       { /* OK: n バイト書き込んだ */ }

gets → fgets

gets は長さを指定できず、攻撃対象になりやすいため C11 で廃止されました。代わりに fgets を使います。

❌ gets (C11で廃止)

char line[64];
gets(line);   // NG: 長さ制限なし

✅ fgets

char line[64];
if (fgets(line, sizeof(line), stdin) == NULL) {
    // EOF またはエラー
}

改行を取り除く

fgets は改行 '\n' もコピーするので、必要なら取り除きます。
char line[64];
if (fgets(line, sizeof(line), stdin)) {
    size_t len = strlen(line);
    if (len > 0 && line[len - 1] == '\n') {
        line[len - 1] = '\0';
    }
}
注意: 入力が64バイトを超えるとき fgets は途中までしか読まない。その場合 line[len-1] != '\n' になるので、ループで残りを読むか、エラー処理するか選ぶ。

atoi → strtol

atoi はエラー検出ができず、オーバーフロー時の挙動も不定です。strtol を使えば「変換失敗」「範囲外」を正しく判定できます。

❌ atoi

int n = atoi(s);
// s="abc" でも 0 を返す → エラーと区別できない
// s="99999999999" でも何かを返す(未定義)

✅ strtol

#include <stdlib.h>
#include <errno.h>

errno = 0;
char *end;
long v = strtol(s, &end, 10);

if (end == s)             { /* 変換できる文字がなかった */ }
else if (*end != '\0')   { /* 途中に変な文字 */ }
else if (errno == ERANGE) { /* long の範囲外 */ }
else                      { /* 成功: 値は v */ }

実用ラッパー

#include <errno.h>
#include <limits.h>
#include <stdlib.h>

// s を int に変換できれば *out にセットし 0 を返す。失敗なら -1。
int parse_int(const char *s, int *out) {
    errno = 0;
    char *end;
    long v = strtol(s, &end, 10);
    if (end == s || *end != '\0') return -1;
    if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return -1;
    *out = (int)v;
    return 0;
}
strtod / strtoul もある: 浮動小数点は strtod、unsigned long は strtoul。いずれも同じパターンで使える。

まとめ表

避けるべき代わりにポイント
strcpysnprintf / strncpy+'\0'サイズ制限なし
strcatstrncat / snprintf同上
sprintfsnprintf戻り値で切り詰め検知
getsfgetsgets はC11で廃止
atoi / atofstrtol / strtodエラー検出可能
scanf("%s", ...)fgets+sscanf または %31s のように幅指定幅なし scanf は危険

コンパイラの警告を有効に

gcc -Wall -Wextra -Wformat-security -Wstack-protector \
    -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 prog.c
-D_FORTIFY_SOURCE=2 は glibc が多くの関数にランタイムチェックを入れてくれる。-fstack-protector-strong はスタック破壊を検知して即座に止める。
静的解析ツール: cppcheckclang-tidyclang --analyze を使うとさらにバッファ系バグを事前に見つけられる。

チャレンジ課題

課題1: strcpy の危険を体験
8バイトの char 配列に18文字の文字列を strcpy でコピーしてみよ。-fstack-protector-strong オプション付きでコンパイルすると stack smashing detected が出る。
課題2: snprintf に置き換え
ユーザー入力を連結して「Hello, [name]! あなたは[age]歳です」を作る関数を、strcpy/strcat で書いた版と snprintf で書いた版で比較。入力が非常に長いときの挙動の違いを確認。
課題3: 安全な整数パーサ
上の parse_int をさらに拡張し、"123abc" のような部分マッチをエラーにする版と、"3.14" を許して整数部だけ取る版の2種類を用意せよ。テストケースも10個ほど書く。
課題4: 安全な行読み込み
char *read_line(FILE *fp, char *buf, size_t size); を実装。入力が size を超えたら残りを捨てて次回以降に影響しないようにする。戻り値は buf か NULL(EOF)。