STR35-C. 長さに制限のないデータを固定長配列へコピーしない
無制限コピーを行う関数は、コピー元が適切なサイズであることに依存している。この前提が成立しないと、バッファオーバーフローを引き起こす。それゆえ、無制限コピーを行う可能性のある関数を使用するときは注意が必要となる。
違反コード (strcat())
strcat() または strcpy() 関数を使用して長さのわからない文字列をサイズの限られたサイズのバッファにコピーすると、バッファのオーバーフローが発生する可能性があり、セキュリティ上の脆弱性の原因の 1 つとなる。以下のコード例では、プログラムのコマンドライン引数の長さが 4096 バイトを超過しないことを想定していて、オーバーフローの可能性をチェックしないで固定サイズのバッファにすべての引数を結合している。strcat() または他の文字列ユーティリティ関数の呼び出しでバッファの最後を越えて書き込みを行おうとした場合、プログラムの動作は未定義となる(C 標準 [ISO/IEC 9899:2011] 附属書 J 「未定義の動作」の 181 を参照)。
int main(int argc, char *argv[]) {
char cmdline [4096];
cmdline[0] = '\0';
for (int i = 1; i < argc; ++i) {
strcat(cmdline, argv [i]);
strcat(cmdline, " ");
}
/* ... */
return 0;
}
適合コード (memcpy())
以下の適合コードでは、コマンドライン引数の最大長について想定することを避け、malloc() 関数と memset() 関数を組み合わせることにより引数の長さに関係なくすべての引数のための十分なスペースを動的に割り当て、割り当てられたスペースに各引数を安全にコピーする。この適合コードは安全であるだけでなくより堅牢でもあり、プログラムのユーザーが不必要に人工的な制限を課されることはない。
int main(int argc, char *argv[]) {
size_t bufsize = 0;
size_t buflen = 0;
char* cmdline = NULL;
for (int i = 1; i < argc; ++i) {
const size_t len = strlen(argv[i]);
if (bufsize - buflen <= len) {
bufsize = (bufsize + len) * 2;
cmdline = realloc(cmdline, bufsize);
if (NULL == cmdline)
return 1; /* realloc 失敗 */
}
memcpy(cmdline + buflen, argv[i], len);
buflen += len;
cmdline[buflen++] = ' ';
}
cmdline[buflen] = '\0';
/* ... */
free(cmdline);
return 0;
}
違反コード (gets())
gets() 関数(C99 の正誤表 3 で廃止予定、C11 で削除)は、stdin からバッファへ読み取るデータ量を制限できないためそもそも危険であり、決して使用してはならない。以下のコード例では、gets() が stdin から BUFSIZ - 1 文字以上読み取らないことを仮定しているが、これは誤った仮定であり、バッファオーバーフローを引き起こす可能性がある。BUFSIZ は stdio.h 内で定義されたマクロ整数定数で、setvbuf() への引数の提案を示していて、このような入力バッファの最大サイズを示しているわけではない。
gets() 関数は、EOF または改行文字まで、stdin からコピー先の配列へ文字を読み取る。遭遇した改行文字は破棄され、配列に読み取られた最後の 1 文字の直後に null 文字が書き込まれる。
char buf[BUFSIZ];
if (gets(buf) == NULL) {
/* エラー処理 */
}
「MSC34-C. 廃止予定の関数や古い関数を使用しない」を参照。
適合コード (fgets())
fgets() 関数は、入力ストリームから配列へ、指定された文字数より 1 文字少ない文字を読み取る。以下のコード例では、stdin から buf へのコピーが割り当てられたメモリを超えることはない。
char buf[BUFFERSIZE];
int ch;
char *p;
if (fgets(buf, sizeof(buf), stdin)) {
/* fgets が成功したので、改行文字をスキャンする */
p = strchr(buf, '\n');
if (p) {
*p = '\0';
}
else {
/* 改行文字が見つからず, stdin を行末まで読み飛ばす */
while (((ch = getchar()) != '\n')
&& !feof(stdin)
&& !ferror(stdin)
);
}
}
else {
/* fgets 失敗、エラー処理する */
}
gets() 関数を fgets() 関数にそのまま置き換えることはできない。なぜなら、fgets() は読み取ったデータ中の改行文字を維持し、さらに、指定されたサイズまでしか読み取らないからである。入力行がコピー先の配列に格納するには長すぎる場合、fgets() を使って安全に処理することはできるが、パフォーマンスの観点からお薦めはできない。gets() を置き換える場合は、次に示すような方法の使用を検討すること。
適合コード (gets_s())
gets_s() 関数は、stdin が指すストリームから配列へ、最大でも指定された文字数より 1 文字少ない文字を読み取る。
C 標準 [ISO/IEC 9899:2011] の附属書 K、セクション K.3.5.4.1 には、次のように記載されている。
改行文字は破棄される。また、改行文字や
EOF以降の文字は読み取られない。破棄された改行文字は、読み取られる文字の数には数えられない。配列に読み取られた最後の文字の直後に null 文字が書き込まれる。
EOF に達しコピー先の配列に文字がまったく読み取られていない場合、あるいは処理中に読み取りエラーが発生した場合、コピー先の配列の 1 文字目に null 文字がセットされ、配列のその他の要素は不定の値をとる。
char buf[BUFSIZ];
if (gets_s(buf, sizeof(buf)) == NULL) {
/* エラー処理 */
}
適合コード (GNU getline())
getline() 関数は GCC で提供される GNU 拡張である。動作は fgets() と同様だが、いくつかの拡張機能が用意されている。まず、入力行が長すぎる場合に入力を切り捨てるのではなく、 realloc() を使用することによってバッファのサイズを変更する。第 2 に、成功した場合、読み取った文字数を返す。これは、改行文字に達する前に入力に null 文字があったかどうかを判断するのに役立つ。
int ch;
char *p;
size_t buffer_size = 10;
char *buffer = malloc( buffer_size);
ssize_t size;
if ((size = getline(&buffer, &buffer_size, stdin)) == -1) {
/* エラー処理 */
} else {
p = strchr(buffer, '\n');
if (p) {
*p = '\0';
} else {
/* 改行文字が見つからず, stdin を行末まで読み飛ばす */
while (((ch = getchar()) != '\n')
&& !feof(stdin)
&& !ferror(stdin)
);
}
}
/* ... バッファの処理 ... */
free (buffer);
getline() は malloc()によって割り当てられたバッファに対してのみ作用する。NULL ポインタを渡された場合、getline() は入力を保持できる十分なサイズのバッファを割り当てる。バッファの解放は、ユーザが free() により後から明示的に行う必要がある。getline() に NULL を渡さないこと。これは、「MEM00-C. メモリの割り当てと解放は、同じ翻訳単位内の同一抽出レベルで行う」に違反する。
違反コード (getchar())
この例では、getchar() 関数を使って stdin から、一度に 1 行ではなく、1 文字ずつ読み取る。stdin ストリームからの文字の読み取りは、EOF に達するか改行文字が読み取られるまでである。遭遇した改行文字は破棄され、配列に読み取られた最後の 1 文字の直後に null 文字が書き込まれる。前の例と同様、このコードがバッファオーバーフローを引き起こさないという保証はない。
char buf[BUFFERSIZE], *p;
int ch;
p = buf;
while ( ((ch = getchar()) != '\n')
&& !feof(stdin)
&& !ferror(stdin))
{
*p++ = ch;
}
*p++ = 0;
適合コード (getchar())
以下の適合コードでは、index = BUFFERSIZE に達すると文字列を null 終端するためのスペースを残してそれ以上文字は buf にコピーされない。while ループは、ファイルの終わりに達したり読み取りエラーが発生したりしない限り、行の終わりまで読み取りを続ける。
unsigned char buf[BUFFERSIZE];
int ch;
int index = 0;
int chars_read = 0;
while ( ( (ch = getchar()) != '\n')
&& !feof(stdin)
&& !ferror(stderr) )
{
if (index < BUFFERSIZE-1) {
buf[index++] = (unsigned char)ch;
}
chars_read++;
} /* while の終了 */
buf[index] = '\0'; /* NTBS の終端 */
if (feof(stdin)) {
/* EOF の場合の処理 */
}
if (ferror(stdin)) {
/* エラー処理 */
}
if (chars_read > index) {
/* 切り捨てが発生した場合の処理 */
}
ループの最後で feof(stdin) != 0 ならば、ループは改行文字に遭遇せずにファイルの終わりまで読み取ったことになる。ループの最後で ferror(stdin) != 0 ならば、ループが改行文字に遭遇する前に読み取りエラーが発生したことになる。ループの最後で chars_read > index ならば、入力文字列は切り捨てられたことになる。この適合コードは、「FIO34-C. 文字入出力関数の返り値の取得には int 型を使用する」にも適合している。
一度に 1 文字ずつ読み取ることで、パフォーマンス上のオーバーヘッドを増やすことなく柔軟に文字列処理を行うことができる。
次に示す while ループのテストで通常は十分である。
while ( ( (ch = getchar()) != '\n') && ch != EOF ) {
feof() と ferror() を使う必要があるケースについては、「FIO35-C. sizeof(int) == sizeof(char) の場合、EOF およびファイルエラーの検出に feof() と ferror() を使用する」を参照のこと。
違反コード (scanf())
以下のコード例では、scanf() 関数を使って stdin からの入力の読み取りとその書式指定を行っている。scanf() を不適切に使用すると、無制限コピーが発生する可能性がある。このコードでは、scanf() の呼び出しは buf に読み取るデータの量を制限していない。10 文字以上読み取られるとバッファオーバーフローが発生する。
enum { CHARS_TO_READ = 9 };
char buf[CHARS_TO_READ + 1];
scanf("%s", buf);
適合コード (scanf())
scanf() が読み取る文字数は、書式指定子を使って制限することができる。
#define STRING(n) STRING_AGAIN(n)
#define STRING_AGAIN(n) #n
#define CHARS_TO_READ 9
char buf[CHARS_TO_READ + 1];
scanf("%"STRING(CHARS_TO_READ)"s", buf);
リスク評価
制限のない入力源から固定サイズのバッファへデータをコピーすると、バッファオーバーフローが発生する可能性がある。
|
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
STR35-C |
高 |
高 |
中 |
P18 |
L1 |
自動検出
|
ツール |
バージョン |
チェッカー |
説明 |
|---|---|---|---|
|
Compass/ROSE |
|
|
このルールの違反を検出できる。 |
|
Coverity |
6.5 |
SECURE_CODING |
実装済み |
| V. 9.1 |
NNTS.TAINTED |
|
|
|
LDRA tool suite |
V. 8.5.4 |
|
|
| PRQA QA-C | 8.1 | warncall for 'gets' | 部分的に実装済み |
関連するガイドライン
| CERT C++ Secure Coding Standard | STR35-CPP. Do not copy data from an unbounded source to a fixed-length array |
| ISO/IEC TS 17961 | (ドラフト) Tainted strings are passed to a string copying function [taintstrcpy] |
| MITRE CWE | CWE-120, Unbounded transfer ("classic buffer overflow") |
参考資料
| [Drepper 2006] | Section 2.1.1, "Respecting Memory Bounds" |
| [ISO/IEC 9899:2011] | Section 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" |
翻訳元
これは以下のページを翻訳したものです。
STR35-C. Do not copy data from an unbounded source to a fixed-length array (revision 135)
