JPCERT コーディネーションセンター

FIO34-C. ファイルから読み込んだ文字と EOF や WEOF を区別する

FIO34-C. ファイルから読み込んだ文字と EOF や WEOF を区別する

EOF マクロは、ファイルからデータを読み込んでいるときに、ファイルの終わりに到達したことを示す負の値として使われる。EOFin-band error indicator の一例である。in-band error indicator の使用は危険であり、「ERR02-C. 正常終了時の値とエラーの値は別の手段で通知する」ではその使用を避けるように勧めている。

入出力関数 fgetc(), getc(), getchar() は、いずれもストリームから文字を読み取り、それを int 型で返す(「STR00-C. 文字の表現には適切な型を使用する」を参照)。ストリームがファイルの終わりに達すると、ストリームのファイル終了フラグが設定され、関数は EOF を返す。読み取りエラーが発生すると、ストリームのエラーフラグが設定され、関数は EOF を返す。EOF は負の値なので、符号無しの型である文字の値と一致することはない。ただし、これは int 型のビット幅が char 型のビット幅よりも大きい場合にだけ成り立つ話である。int 型と char 型のビット幅が同じ場合、文字を読み込む関数は EOF と全く同じビットパターンの文字を返す可能性がある。これは例えば、攻撃者がファイルやデータストリームに EOF に見える値を紛れ込ませた場合に発生する。C 言語規格で要請されているのは、int 型が +32767 を表現できること、char 型のビット幅は int 型を超えないこと、だけだ。あまり一般的ではないが、sizeof(int) == sizeof(char) となるような環境では (int)(unsigned char)65535 == -1 となり、EOF は通常の文字と見分けがつかない。したがってこのような環境では、ファイルの終わりやエラーの発生をチェックする feof()ferror() 関数を使わなければ、EOF の扱いを誤ってしまう可能性がある。

ワイド文字の読込みでは、より多くの環境でこの問題が発生する。fgetwc()getwc()getwchar()wint_t 型の値を返す。wint_t 型の値は読み込んだワイド文字の値を表現する型であるとともに、ワイド文字のストリームの終わりを示す WEOF も表現できる型である。多くの実装では wchar_t 型と wint_t 型は同じ大きさであり、これらの関数の返り値が通常の文字なのか、ストリームの終わりを示す WEOF なのか区別できない状況が発生する可能性がある。

UTF-16 符号化形式では、0xFFFF は文字ではないことが保証されているため、WEOF-1 で表すことが可能である。同様に UTF-32 符号化形式では、全ての文字コードは符号付き32ビット整数として正の値を持つ。現在広く使われている文字集合(符号化形式)では、文字ではないコードが少なくとも1つあるようにデザインされている。したがって、ワイド文字あるいは int 型と同じ大きさの通常の(ワイド文字でない)文字で、この問題が発生することはまずないと言っていいだろう。

C 標準ライブラリの feof() 関数と ferror() 関数は文字型と整数型の大きさの問題には影響されないので、ファイルの終わりやファイルエラーをチェックするときはこれらの関数を使うべきである[Kettlewell 2002]。ただし、繰り返し処理のなかで毎回2つの関数を呼ぶとオーバーヘッドがかかる。繰り返し処理のなかでは EOFWEOF はファイルの終わりやエラーを示しているものとして扱い、繰り返し処理の後で feof()ferror() を使い、正しい処理を行っていたかを確認する、という方法が良いだろう。

違反コード

次の違反コード例では、文字 cEOF になるまで処理を繰り返している。

#include <stdio.h>

void func(void) {
  int c;

  do {
    c = getchar();
  } while (c != EOF);
}

EOF は負の値であり、他の正の値を持つ文字と区別できることは保証されているが、入力した文字データを int に変換して比較する場合、正しく EOF と区別できるとは限らない。int 型と char 型が同じ大きさの環境では、繰り返し処理が実際の入力データの終わりまで処理を行わずに終了してしまう可能性がある。

適合コード (ポータブルなコード)

次の適合コードでは feof() を使ってファイルの終わりをチェックし、ferror() を使ってエラーが発生していないかチェックしている。

#include <stdio.h>

void func(void) {
  int c;

  do {
    c = getchar();
  } while (c != EOF);
  if (feof(stdin)) {
    /* ファイルの終わりだった場合の処理 */
  } else if (ferror(stdin)) {
    /* ファイルに関するエラーだった場合の処理 */
  } else {
    /* EOF を入力として受け取っていた場合の処理 */
  }
}
違反コード (ポータブルでないコード)

次に示す違反コード例ではアサーションを使って、int 型が char より大きい環境でのみ実行が行われるようにしている。また、EOF は有効な文字の値とは異なることが保証されている。しかし、このコード例では、変数 cint 型ではなく char 型と宣言されている。単なる char 型が符号付きの型として扱われる環境の場合、int 型への変換において符号拡張が行われるため、入力された文字の値を EOF と比較したときに一致してしまう可能性がある。

#include <assert.h>
#include <limits.h>
#include <stdio.h>

void func(void) {
  char c;
  static_assert(UCHAR_MAX < UINT_MAX, "FIO34-C violation");

  do {
    c = getchar();
  } while (c != EOF);
}

char が符号付き8ビット幅の型、int が32ビット幅の型である場合、getchar() が文字コード 0xFF (10進で255) の文字を返したとすると、これは EOF と解釈されてしまう。なぜならこの値は比較演算の際に 0xFFFFFFFF (EOF の値) に符号拡張されるからである(「STR34-C. 文字データをより大きなサイズの整数型に変換するときは事前に unsigned char 型に変換する」を参照のこと)。

適合コード (ポータブルでないコード)

次に示す適合コードでは、変数 cint 型と宣言している。これによって、繰り返し処理は、ファイルの終わりまで行われるようになる。

#include <assert.h>
#include <stdio.h>
#include <limits.h>

void func(void) {
  int c;
  static_assert(UCHAR_MAX < UINT_MAX, "FIO34-C violation");

  do {
    c = getchar();
  } while (c != EOF);
}
違反コード (ワイド文字)

次の違反コード例では、C 標準ライブラリ関数 getwc() の呼出しの結果が wchar_t 型の変数に格納され、WEOF と比較されている。

#include <stddef.h>
#include <stdio.h>
#include <wchar.h>

enum { BUFFER_SIZE = 32 };

void g(void) {
  wchar_t buf[BUFFER_SIZE];
  wchar_t wc;
  size_t i = 0;

  while ((wc = getwc(stdin)) != L'\n' && wc != WEOF) {
    if (i < (BUFFER_SIZE - 1)) {
      buf[i++] = wc;
    }
  }
  buf[i] = L'\0';
}

このコードには2つ問題がある。1つめは、getwc() の返り値が、WEOF との比較演算の前に wchar_t に変換されていることである。2つめは、wint_twchar_t より大きい型であることを確認していないことである。これらの問題により、ファイル中に WEOF と一致する値となるデータを入れておくことで、ファイルの途中で処理を終了させることが可能になってしまう。

適合コード (ポータブルなコード)

次に示す適合コードでは、変数 cwint_t 型と宣言し、getwc() の返り値の整数型に合わせている。さらに、ファイルの終わりを判別するために、WEOF の識別以外の方法も使っている。

#include <stddef.h>
#include <stdio.h>
#include <wchar.h>

enum {BUFFER_SIZE = 32 }

void g(void) {
  wchar_t buf[BUFFER_SIZE];
  wint_t wc;
  size_t i = 0;

  while ((wc = getwc(stdin)) != L'\n' && wc != WEOF) {
    if (i < BUFFER_SIZE - 1) {
      buf[i++] = wc;
    }
  }

  if (feof(stdin) || ferror(stdin)) {
   buf[i] = L'\0';
  } else {
    /* WEOF を入力として受け取っていた場合の処理 */
  }
}
例外

FIO34-EX1: 正常に処理が行われた結果として文字列を返し、エラー終了時には EOF を返す C の関数はいくつもある。これらには次のような関数も含まれる。fclose()fflush()fputs()fscanf()puts()scanf()sscanf()vfscanf()vscanf()。これらの関数呼出しにおけるエラー判定は、返り値を EOF と比較すればよい。

リスク評価

ファイルに含まれる文字データが EOFWEOF と区別できるという誤解から様々な脆弱性が発生した。コマンドインジェクション攻撃などはその一例である(CA-1996-22 を参照)。

ルール

深刻度

可能性

修正コスト

優先度

レベル

FIO34-C

P12

L1

自動検出(最新の情報はこちら

ツール

バージョン

チェッカー

説明

CodeSonar 4.0 LANG.CAST.COERCE Coercion Alters Value

Compass/ROSE

 

 

 

Coverity

6.5

CHAR_IO

fgetc()getc()getchar() の返り値が int ではなくchar に誤って代入されているケースを検出する。Coverity Prevent は、このルールの違反をすべて検出できるわけではないため、さらなる検証が必要である。

ECLAIR

1.2

CC2.FIO34

一部実装済み

Fortify SCA

5.0

 

CERT C Rule Pack を使ってこのルールの違反を検出できる。

Splint

3.1.1

 

 

関連するガイドライン
CERT C セキュアコーディングスタンダード STR00-C. 文字の表現には適切な型を使用する
INT31-C. 整数変換によってデータの消失や解釈間違いが発生しないことを保証する
CERT C++ Secure Coding Standard FIO34-CPP. Use int to capture the return value of character IO functions
FIO35-CPP. Use feof() and ferror() to detect end-of-file and file errors when sizeof(int) == sizeof(char) 
Java セキュアコーディングスタンダード CERT/Oracle 版

FIO08-J. 文字やバイトを読み取るメソッドの返り値は int で受ける

ISO/IEC TS 17961:2013 Using character values that are indistinguishable from EOF [chreof]
参考資料
[Kettlewell 2002] Section 1.2, "<stdio.h> and Character Types"
[NIST 2006] SAMATE Reference Dataset Test Case ID 000-000-088
[Summit 2005] Question 12.2
翻訳元

これは以下のページを翻訳したものです。

FIO34-C. Distinguish between characters read from a file and EOF or WEOF (revision 17)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter