STR31-C. 文字データと null 終端文字を格納するために十分な領域を確保する
データをバッファにコピーする際、データを保持する十分な大きさがバッファにないと、バッファオーバーフローが発生する。バッファオーバーフローは、文字列の処理を行う際にしばしば発生する[Seacord 2013b]。このようなエラーを防ぐには、コピーする文字列を切り詰めるか、あるいは可能であればコピーする文字データと null 終端文字を保持できる十分なサイズをコピー先に確保すること(「STR03-C. null 終端バイト文字列を不注意に切り捨てない」を参照)。
違反コード (オフバイワンエラー)
以下に、一般に オフバイワンエラー と呼ばれるコード例を示す[Dowd 2006]。ループ処理では、src
から dest
へデータをコピーしている。しかし、このループ処理では null 終端文字を考慮していないため、書き込まれるデータは dest
の終端を 1 バイト越える可能性がある。
#include <stddef.h> enum { ARRAY_SIZE = 32 }; void func(void) { char dest[ARRAY_SIZE]; char src[ARRAY_SIZE]; size_t i; for (i = 0; src[i] && (i < sizeof(dest)); ++i) { dest[i] = src[i]; } dest[i] = '\0'; }
適合コード (オフバイワンエラー)
この適合コードでは、ループの終了条件が変更され、null 終端文字を考慮して dest
への書き込みを行っている。
#include <stddef.h> enum { ARRAY_SIZE = 32 }; void func(void) { char dest[ARRAY_SIZE]; char src[ARRAY_SIZE]; size_t i; for (i = 0; src[i] && (i < sizeof(dest) - 1); ++i) { dest[i] = src[i]; } dest[i] = '\0'; }
違反コード (gets()
)
gets()
は C99 Tecnical Corrigendum 3 で非推奨関数とされ、C11 で削除された関数である。この関数は stdin
からバッファに読み込むデータの量を制御する方法がないため本質的に危険であり、使うべきでない。以下のコード例では、gets()
が BUFFER_SIZE - 1
文字までしか読み込まないものと誤った想定をしており、バッファオーバフローが発生する可能性がある。
gets()
関数は stdin
から文字の列を読み込んで引数が指すバッファに書き込む。stdin
からの読み込みは、ファイルの終わり(end-of-file)あるいは改行文字が読み込まれるまで続く。読み込んだ改行文字は捨て、バッファに書き込んだ最後の文字の次に null 文字を書き込む。
#include <stdio.h> #define BUFFER_SIZE 1024 void func(void) { char buf[BUFFER_SIZE]; if (gets(buf) == NULL) { /* エラー処理 */ } }
「MSC24-C. 非推奨関数や時代遅れの関数を使用しない」も参照。
適合コード (fgets()
)
fgets()
関数は、指定された文字数 - 1 までしか読み込まないため、この適合コードでは、stdin
から buf
にコピーされる文字の列は割り当てられたメモリの範囲を越えることはない。
#include <stdio.h> #include <string.h> enum { BUFFERSIZE = 32 }; void func(void) { char buf[BUFFERSIZE]; int ch; if (fgets(buf, sizeof(buf), stdin)) { /* fgets() 正常終了. 改行文字をさがす */ char *p = strchr(buf, '\n'); if (p) { *p = '\0'; } else { /* 改行文字は見つからなかった. 行末まで読み捨てる */ while ((ch = getchar()) != '\n' && ch != EOF) ; if (ch == EOF && !feof(stdin) && !ferror(stdin)) { /* ファイルの終わりでなく EOF 文字を読み込んでいた場合の処理 */ } } } else { /* fgets() 失敗. エラー処理 */ } }
fgets()
関数は厳密な意味では gets()
関数を置き換えるものではない。なぜならば、改行文字を読み込んだときには書き込み先にも改行文字をコピーするし、読み込む行が長い場合には途中までしか読み込まないからである。バッファに収まらないほど長い入力行を安全に処理するために fgets()
を使うことは可能だが、パフォーマンスの観点からはすすめられる方法ではない。gets()
を置き換える際には、次のふたつの適合コードのどちらかを適用することを検討せよ。
適合コード (gets_s()
)
gets_s()
関数は、指定された文字数より一文字だけ少ない文字数まで stdin
が指すストリームから読み込んで配列に書き込む。
C 言語規格の付録 K [ISO/IEC 9899:2011] には次のように記載されている。
改行文字あるいはファイルの終わりまで読み込む。改行文字は捨てられ、読み込んだ文字数にはカウントされない。配列に読み込んだ文字の次に null 文字が書き込まれる。
ファイルの終わりに遭遇して一文字も読み込まなかった場合あるいは読み込みエラーが発生した場合には、配列の先頭に null 文字が書き込まれ、配列の残りの部分の値は不定となる。
#define __STDC_WANT_LIB_EXT1__ 1 #include <stdio.h> enum { BUFFERSIZE = 32 }; void func(void) { char buf[BUFFERSIZE]; if (gets_s(buf, sizeof(buf)) == NULL) { /* エラー処理 */ } }
適合コード (getline()
, POSIX)
getline()
関数は fgets()
と類似の関数であるが、入力データを書き込むバッファを動的に割り当てる。第1引数に null ポインタを渡された場合には、入力データを収めるに十分な大きさのメモリを割り当てる。第1引数に動的に割り当てられたメモリ領域へのポインタが渡され、そのサイズ(第2引数で渡される)が入力データを収めるに十分でない場合には、入力データを切り詰めるのではなく、realloc()
関数を使ってメモリ領域のサイズを変更する。正常終了した場合、getline()
関数は読み込んだ文字数を返す。これを使えば、入力データ中、改行コードの前に null 文字がはいっていたかどうかを調べることができる。getline()
関数は動的に割り当てられたバッファでのみ正常に動作する。メモリリークを防ぐために、割り当てられたメモリは関数の呼び出し側で明示的に解放しなければならない(「MEM31-C. 動的に割り当てられたメモリは一度だけ解放する」を参照)。
#include <stdio.h> #include <stdlib.h> #include <string.h> void func(void) { int ch; size_t buffer_size = 32; char *buffer = malloc(buffer_size); if (!buffer) { /* Handle error */ return; } if ((ssize_t size = getline(&buffer, &buffer_size, stdin)) == -1) { /* エラー処理 */ } else { char *p = strchr(buffer, '\n'); if (p) { *p = '\0'; } else { /* 改行文字は見つからず. 行末まで stdin を読み捨てる */ while ((ch = getchar()) != '\n' && ch != EOF) ; if (ch == EOF && !feof(stdin) && !ferror(stdin)) { /* 入力中に EOF 文字があった. エラー処理 */ } } } free (buffer); }
getline()
関数の処理でなんらかのエラーが発生した場合、返り値がエラーを示す値となる。getline()
のこの挙動は「ERR02-C. 正常終了時の値とエラーの値は別の手段で通知する」に違反している。
違反コード (getchar()
)
パフォーマンスは悪くなるが、1文字ずつ読み込むことにより、より柔軟に挙動を制御できる。次に示す違反コードは一行分のデータを一度に読み込む代わりに、getchar()
関数を使ってstdin
から1文字ずつ読み込んでいる。stdin
ストリームからの読み込みは、ファイルの終わりまであるいは改行コードが現れるまで続く。改行コードは読み捨てられ、最後にバッファに書き込まれた文字の直後に null 文字が書き込まれる。gets()
を使った違反コードと同様に、バッファオーバフローが発生しないという保証はない。
#include <stdio.h> enum { BUFFERSIZE = 32 }; void func(void) { char buf[BUFFERSIZE]; char *p; int ch; p = buf; while ((ch = getchar()) != '\n' && ch != EOF) { *p++ = (char)ch; } *p++ = 0; if (ch == EOF) { /* EOF を読み込んだ場合の処理, あるいはエラー処理 */ } }
ループが終了した後 ch == EOF
であれば、改行コードに遭遇しないままストリームの終わりまで読み込んだか、改行コードに遭遇する前に読み込みエラーが発生したことになる。「FIO34-C. ファイルから読み込んだ文字と EOF もしくは WEOF を区別する」に適合するためには、エラー処理のコードは、feof()
や ferror()
を使ってどちらの状況なのかをチェックしなければならない。
適合コード (getchar()
)
次の適合コードでは、index == BUFFERSIZE - 1
となったらコピーを終了し、null 終端する余地を残している。ループ処理は、行末かファイルの終わりかエラーが発生するまで読み込みを続ける。chars_read > index
だった場合、入力文字列は途中で切り捨てられている。
#include <stdio.h> enum { BUFFERSIZE = 32 }; void func(void) { char buf[BUFFERSIZE]; int ch; size_t index = 0; size_t chars_read = 0; while ((ch = getchar()) != '\n' && ch != EOF) { if (index < sizeof(buf) - 1) { buf[index++] = (char)ch; } chars_read++; } buf[index] = '\0'; /* null 終端する */ if (ch == EOF) { /* EOF を読み込んだ場合の処理, あるいはエラー処理 */ } if (chars_read > index) { /* 入力の切り捨てが発生した場合の処理 */ } }
違反コード (fscanf()
)
次の違反コードでは、fscanf()
呼出しが配列 buf
をはみ出して書き込みを行う可能性がある。
#include <stdio.h> enum { BUF_LENGTH = 1024 }; void get_data(void) { char buf[BUF_LENGTH]; if (1 != fscanf(stdin, "%s", buf)) { /* エラー処理 */ } /* 関数の残りの部分 */ }
適合コード (fscanf()
)
次の適合コードでは、fscanf()
呼出しによる書き込みが buf
をはみ出さないように制限している。
#include <stdio.h> enum { BUF_LENGTH = 1024 }; void get_data(void) { char buf[BUF_LENGTH]; if (1 != fscanf(stdin, "%1023s", buf)) { /* エラー処理 */ } /* 関数の残りの部分 */ }
違反コード (argv
)
ホスト環境では、コマンドラインから読み取られた引数はプロセスメモリに格納される。プログラムでコマンドライン引数を受け取る場合、プログラムの起動時に呼び出される関数 main()
は次のように宣言される。
int main(int argc, char *argv[]) { /* ... */ }
コマンドライン引数は、個々の文字列へのポインタが配列メンバ argv[0]
から argv[argc-1]
に格納された形で main()
に渡される。argc
の値が 0 より大きい場合、argv[0]
が指す文字列はプログラム名である。argc
が 1 より大きい場合、argv[1]
から argv[argc-1]
までのポインタは実引数を指している。
コマンドライン引数や他のプログラム入力のコピー先として、適切なサイズのメモリ領域が割り当てられないと、脆弱性が生じる可能性がある。次の違反コード例では、攻撃者が argv[0]
の内容を操作することで、バッファオーバーフローが発生する可能性がある。
#include <string.h> int main(int argc, char *argv[]) { /* argv[0] が null でないことを確認 */ const char *const name = (argc && argv[0]) ? argv[0] : ""; char prog_name[128]; strcpy(prog_name, name); return 0; }
適合コード (argv
)
次の適合コードでは、argv[0]
から argv[argc - 1]
によって参照される文字列の長さを strlen()
関数を使って確認し、適切なサイズのメモリ領域を動的に割り当てている。
#include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { /* argv[0] が null でないことを確認 */ const char *const name = (argc && argv[0]) ? argv[0] : ""; char *prog_name = (char *)malloc(strlen(name) + 1); if (prog_name != NULL) { strcpy(prog_name, name); } else { /* エラー処理 */ } free(prog_name); return 0; }
null 終端バイト文字を考慮して、書き込み先のメモリ領域のサイズは文字列のサイズに 1 バイト加算すること。
適合コード (argv
)
strcpy_s()
関数は、より安全にコピーを行うためにコピー先バッファのサイズも引数として受け取る(「STR07-C. 境界チェックインタフェースを使用し、文字列操作を行う既存のコードの脅威を緩和する」を参照)。
#define __STDC_WANT_LIB_EXT1__ 1 #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { /* argv[0] が null でないことを確認 */ const char *const name = (argc && argv[0]) ? argv[0] : ""; char *prog_name; size_t prog_size; prog_size = strlen(name) + 1; prog_name = (char *)malloc(prog_size); if (prog_name != NULL) { if (strcpy_s(prog_name, prog_size, name)) { /* エラー処理 */ } } else { /* エラー処理 */ } /* ... */ free(prog_name); return 0; }
strcpy_s()
関数を使うことで、動的に割り当てられたメモリと静的に割り当てられた配列との間でデータをコピーできる。メモリ領域のサイズが十分でない場合、strcpy_s()
はエラーを返す。
適合コード (argv
)
引数の文字列を変更したり連結したりしないのであれば、コピーする必要はない。文字列コピーをしないことが、バッファオーバフローを防ぐためにも、また、プログラムの実行効率の観点からも最良の方法である。argv[0]
が null でないとは限らないことに注意。
int main(int argc, char *argv[]) { /* argv[0] が null でないことを確認 */ const char * const prog_name = (argc && argv[0]) ? argv[0] : ""; /* ... */ return 0; }
違反コード (getenv()
)
C 言語規格の 7.22.4.6 [ISO/IEC 9899:2011] によれば:
getenv
関数はホスト環境が提供する環境の並び(environment list)の中で,name
が指す文字列と一致する文字列を探索する。環境の並びに定義されている名前の集合及び環境の並びを変更する方法は, 処理系定義とする。
環境変数は任意のサイズを持つため、環境変数のサイズ確認と適切なメモリ割り当てを行わずに固定長配列へコピーすると、バッファオーバーフローを引き起こす可能性がある。
#include <stdlib.h> #include <string.h> void func(void) { char buff[256]; char *editor = getenv("EDITOR"); if (editor == NULL) { /* 環境変数 EDITOR が定義されていない場合のエラー処理 */ } else { strcpy(buff, editor); } }
適合コード (getenv()
)
環境変数は、プログラムがロードされるときにプロセスメモリへ読み込まれる。結果として、これらの文字列のサイズは、strlen()
関数を呼び出すことで取得でき、得られたサイズを使って適切な動的メモリ割り当てを行うことができる。
#include <stdlib.h> #include <string.h> void func(void) { char *buff; char *editor = getenv("EDITOR"); if (editor == NULL) { /* 環境変数 EDITOR が定義されていない場合のエラー処理 */ } else { size_t len = strlen(editor) + 1; buff = (char *)malloc(len); if (buff == NULL) { /* エラー処理 */ } memcpy(buff, editor, len); free(buff); } }
違反コード (sprintf()
)
以下の違反コードの name
変数は、ユーザ入力、ファイルシステム、ネットワークなど、外部から与えられた文字列を指しているとする。ファイルをオープンするために、プログラムは name 変数を使ってファイル名を表す文字列を組み立てている。
#include <stdio.h> void func(const char *name) { char filename[128]; sprintf(filename, "%s.txt", name); }
sprintf()
関数は、生成する文字列の長さを制限しないため、name
変数に想定を超える長さの文字列が入っているとバッファオーバーフローが発生する。
適合コード (sprintf()
)
バッファオーバーフローを防ぐには、書式指定子 %s
に精度(precision)を指定すればよい。123
を指定することで、name
変数の先頭から123文字までが使われ、そこに拡張子 .txt
と null 終端文字をつなげて filename
が作られる。
#include <stdio.h> void func(const char *name) { char filename[128]; sprintf(filename, "%.123s.txt", name); }
適合コード (snprintf()
)
より一般的な対策は snprintf()
関数を使用することだ。
#include <stdio.h> void func(const char *name) { char filename[128]; snprintf(filename, sizeof(filename), "%s.txt", name); }
リスク評価
十分なサイズを持たないバッファに文字列データをコピーすると、バッファオーバーフローを引き起こす。攻撃者はこれを悪用し、脆弱なプロセスの権限で任意のコードを実行する可能性がある。
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
STR31-C |
高 |
高 |
中 |
P18 |
L1 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
|
|
このルールの違反を検出できる。しかし、 |
|
Coverity | 6.5 |
STRING_OVERFLOW STRING_SIZE SECURE_CODING |
実装済み 実装済み 実装済み |
5.0 |
|
|
|
9.1 |
NNTS.TAINTED |
|
|
LDRA tool suite |
8.5.4 |
|
|
3.1.1 |
|
|
|
PRQA QA-C | 8.1 | warncall for 'gets' | 一部実装済み |
関連する脆弱性
CVE-2009-1252 はこのルールへの違反に起因する脆弱性である。バージョン 4.2.4p7 および 4.2.5p74 より前の ntpd に含まれる sprintf
の呼び出しでは文字配列をオーバーフローさせることができ、攻撃者は任意のコードを実行することが可能となってしまう [xorl 2009]。
CVE-2009-0587 はこのルールへの違反に起因する脆弱性である。バージョン 2.24.5 より前の Evolution Data Server では、ユーザ入力文字列の長さに関する整数演算を行い、その計算結果をそのまま使って新たなメモリ領域を割り当てていた。攻撃者は、長い文字列を入力することで不適切なメモリ割り当てとそれに続くバッファオーバフローを発生させ、任意のコードを実行することが可能であった[xorl 2009]。
関連するガイドライン
CERT C Secure Coding Standard |
STR03-C. Do not inadvertently truncate a string |
CERT C++ Secure Coding Standard | STR31-CPP. Guarantee that storage for character arrays has sufficient space for character data and the null terminator |
ISO/IEC TR 24772:2013 | String Termination [CJM] Buffer Boundary Violation (Buffer Overflow) [HCB] Unchecked Array Copying [XYW] |
ISO/IEC TS 17961:2013 |
Using a tainted value to write to an object using a formatted input or output function [taintformatio] |
MITRE CWE | CWE-119, Improper Restriction of Operations within the Bounds of a Memory Buffer CWE-120, Buffer Copy without Checking Size of Input ("Classic Buffer Overflow") CWE-193, Off-by-one Error |
参考資料
[Dowd 2006] | Chapter 7, "Program Building Blocks" ("Loop Constructs," pp. 327–336) |
[Drepper 2006] | Section 2.1.1, "Respecting Memory Bounds" |
[ISO/IEC 9899:2011] | K.3.5.4.1, "The gets_s Function" |
[Lai 2006] | |
[NIST 2006] | SAMATE Reference Dataset Test Case ID 000-000-088 |
[Seacord 2013] | Chapter 2, "Strings" |
[xorl 2009] | FreeBSD-SA-09:11: NTPd Remote Stack Based Buffer Overflows |
翻訳元
これは以下のページを翻訳したものです。
STR31-C. Guarantee that storage for strings has sufficient space for character data and the null terminator (revision 167)