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

FIO02-C. 汚染された情報源から取得したパス名は正規化する

パス名、ディレクトリ名、およびファイル名には、検証することが難しい文字や正しくない文字が含まれていることがある。また、パス名の一部がシンボリックリンクの場合は、ファイルの実際の場所や同一性がさらにわかりにくい。ファイル名の検証を容易にするため、名前を正規化した形式に変換することを推奨する。ファイル名を正規化することで、パス名、ディレクトリ名、ファイル名を比較しやすくなり、検証がはるかに容易になる。

正規化形式はオペレーティングシステムやファイルシステムごとに異なる。オペレーティングシステム固有の仕組みを使用して正規化するのが望ましい。

一例として、パス名が POSIX システム上のユーザのホームディレクトリ内のファイルを参照することを確認する関数を示す。

#include <pwd.h>
#include <unistd.h>
#include <string.h>

int verify_file(char *const filename) {
  /* 現在のユーザの /etc/passwd エントリを取得 */
  struct passwd *pwd = getpwuid(getuid());
  if (pwd == NULL) {
    /* エラー処理 */
    return 0;
  }

  const size_t len = strlen( pwd->pw_dir);
  if (strncmp( filename, pwd->pw_dir, len) != 0) {
    return 0;
  }
  /* homedir の直後に '/', が 1 つだけあることを確認する */
  if (strrchr( filename, '/') == filename + len) {
    return 1;
  }
  return 0;
}

verify_file() 関数に渡されるファイル名は絶対パス名でなければならない。また、参照先のファイル名が実際にはユーザのホームディレクトリにはないファイル名へのシンボリックリンクであると、関数の処理結果は間違ってしまう。

違反コード

以下のコード例では、argv[1] には、信頼できない情報源から取得されたファイル名が含まれ、書き込み用にオープンされる。ファイル操作にこのファイルを使用する前に、想定どおりの有効なファイルを参照していることを検証する必要がある。残念ながら、argv[1] が参照するファイル名には、ディレクトリ文字など検証が不可能ではないにしても難しい特殊文字が含まれている可能性がある。さらに、argv[1] 内のパス名の一部はシンボリックリンクであるかもしれず、検証をパスしてもそのファイル名が無効なファイルを指している可能性がある。

検証が正しく行われない場合、fopen() の呼び出しは、意図しないファイルへのアクセスにつながることがある。

/* argv[1] が設定されていることを確認する */

if (!verify_file(argv[1]) {
  /* エラー処理 */
}

if (fopen(argv[1], "w") == NULL) {
  /* エラー処理 */
}

/* ... */
適合コード (POSIX)

ファイル名の正規化は難しく、根底にあるファイルシステムを理解している必要がある。

POSIX の realpath() 関数を使用してパス名を正規化形式に変換できる。POSIX 規格には次のように記載されている[Open Group 2004]。

realpath() 関数は、file_name が指すパス名から、同じファイルを指定する絶対パス名を取得する。取得した名前には、「.」、「..」、シンボリックリンクは含まれない。

さらに、連続する 2 つのスラッシュや予期せぬ特殊ファイルがファイル名に含まれないかを検証する必要もある。パス名の展開方法の詳細は、POSIX のセクション 4.12、「Pathname Resolution」を参照[Open Group 2004]。

realpath() 関数の man ページには、Linux Programmer's Manual [Linux 2008] の以下の警告のように、注意事項が記載されていることが多い。

この関数の使用は避けること。(非標準の resolved_path == NULL 機能を使用しない限り、)出力バッファ resolved_path の適切なサイズを判断することができないという設計上の問題があるためである。POSIX では、PATH_MAX のサイズのバッファで十分ということになっているが、PATH_MAX を定数として定義することは要求されておらず、pathconf(3) を使用して取得しなければならないことがある。また、pathconf(3) を使っても実際のところは意味がない。というのも、POSIX は pathconf(3) の返り値が、malloc で確保するには適していない巨大な値になることがあると警告する一方で、pathconf(3) が −1 を返すことで、PATH_MAX に上限がないことを示すとしているからである。

libc4 と libc5 の実装には、バッファオーバーフローが含まれている(libc-5.4.13 で修正済み)。その結果、mount(8) のような set-user-ID プログラムでは何らかの対策を行ったバージョンを使うべきである。

POSIX.1-2008 で realpath() 関数の仕様が変更された。他の POSIX バージョンでは、resolved_name が NULL ポインタの場合は、処理系定義の動作が認められていた。現行の POSIX リビジョン、および最新の処理系の多くは(glibc や Linux を筆頭に)、この引数に NULL ポインタが使用された場合は正規化済みの名前を格納するためのメモリを割り当てる。

以下のコードを使用して、realpath() 関数のこの改訂版に依存するコードを条件付きでインクルードすることができる。

#if _POSIX_VERSION >= 200809L || defined (linux)

ゆえに、以下に示すように、resolved_nameNULL を設定して realpath() を呼び出すことは(これが使用可能なシステム上では)安全である。

char *realpath_res = NULL;

/* argv[1] が設定されていることを確認する */

realpath_res = realpath(argv[1], NULL);
if (realpath_res == NULL) {
  /* エラー処理 */
}

if (!verify_file(realpath_res) {
  /* エラー処理 */
}

if (fopen(realpath_res, "w") == NULL) {
  /* エラー処理 */
}

/* ... */

free(realpath_res);
realpath_res = NULL;

また、PATH_MAX<limits.h> で定数として定義されている場合は、NULL 以外の resolved_path を指定して、realpath() を呼び出すことも安全である。この場合、realpath() 関数は、resolved_path が正規化したパスを格納できる大きさの文字配列を参照していると想定している。PATH_MAX が定義されている場合、以下の適合コードに示すように、PATH_MAX のサイズのバッファを割り当てて、realpath() の結果を格納する。

char *realpath_res = NULL;
char *canonical_file name = NULL;
size_t path_size = 0;

/* argv[1] が設定されていることを確認する */

path_size = (size_t)PATH_MAX;

if (path_size > 0) {
  canonical_filename = malloc(path_size);

  if (canonical_filename == NULL) {
    /* エラー処理 */
  }

  realpath_res = realpath(argv[1], canonical_filename);
}

if (realpath_res == NULL) {
  /* エラー処理 */
}

if (!verify_file(realpath_res) {
  /* エラー処理 */
}
if (fopen(realpath_res, "w") == NULL ) {
  /* エラー処理 */
}

/* ... */

free(canonical_filename);
canonical_filename = NULL;

realpath() を使用してファイル名を検査する場合にも、TOCTOU (Time-Of-Check, Time-Of-Use) 状態を避けるように注意する必要がある。

違反コード (POSIX)

PATH_MAX が定数として定義されていない場合に、NULL 以外の resolved_path を指定して realpath() を呼び出すのは危険である。POSIX.1-2008 では、この種の realpath() の用法を実質的に禁止している[Austin Group 2008]。

resolved_name が NULL ポインタ以外で、PATH_MAX<limits.h> ヘッダで定数として定義されていない場合、動作は未定義である。

POSIX.1-2008 には、この用法が危険な理由が説明されている[Austin Group 2008]。

realpath() には length 引数がないため、PATH_MAX<limits.h> で定数として定義されていない場合、アプリケーションは realpath() に安全に渡すために必要なバッファサイズを判断することができない。過去に pathconf() の呼び出しで取得した PATH_MAX の値は、realpath() を呼び出す時点では古くなっている。したがって、PATH_MAX<limits.h> で定義されていない場合に realpath() を使用するための、唯一信頼できる方法は、resolved_name に NULL ポインタを設定することで、必要なサイズのバッファを realpath() に割り当てさせることである。

PATH_MAX の値はファイルシステム毎に異なる(そのため、sysconf() ではなく、pathconf() を使用して取得する)。たとえば、パス内のディレクトリが他のファイルシステムへのシンボリックリンクに置き換えられた場合や、新しいファイルシステムがパス内にマウントされた場合など、過去の pathconf() の呼び出しで取得した PATH_MAX の値が無効になる場合がある。

char *realpath_res = NULL;
char *canonical_filename = NULL;
size_t path_size = 0;
long pc_result;

/* argv[1] が設定されていることを確認する */

errno = 0;

/* PATH_MAX の問い合わせ */
pc_result = pathconf(argv[1], _PC_PATH_MAX);

if ( (pc_result == -1) && (errno != 0) ) {
  /* エラー処理 */
} else if (pc_result == -1) {
  /* エラー処理 */
} else if (pc_result <= 0) {
  /* エラー処理 */
}
path_size = (size_t)pc_result;

if (path_size > 0) {
  canonical_filename = malloc(path_size);

  if (canonical_filename == NULL) {
    /* エラー処理 */
  }

  realpath_res = realpath(argv[1], canonical_filename);
}

if (realpath_res == NULL) {
  /* エラー処理 */
}

if (!verify_file(realpath_res) {
  /* エラー処理 */
}

if (fopen(realpath_res, "w") == NULL) {
  /* エラー処理 */
}

/* ... */

free(canonical_filename);
canonical_filename = NULL;
処理系固有の詳細 (Linux)

libc4libc5realpath() の実装には、バッファオーバーフローが含まれている(libc-5.4.13 で修正済み)[VU#743092]。ゆえに、プログラムで使う場合、この問題が修正されたことが分かっている独自のバージョンが必要である。

適合コード (glibc)

realpath() 関数は使用が難しく、性能が良くないことがある。別の解決法として、GNU 拡張の canonicalize_file_name() がある。この関数は realpath() と同じ効果があるが、結果は常に新たに割り当てられたバッファに返される [Drepper 2006]。

/* argv[1] が設定されていることを確認する */

char *canonical_filename = canonicalize_file_name(argv[1]);
if (canonical_filename == NULL) {
  /* エラー処理 */
}

/* ファイル名を確認する */

if (fopen(canonical_filename, "w") == NULL) {
  /* エラー処理 */
}

/* ... */

free(canonical_filename);
canonical_filename = NULL;

canonicalize_file_name() によってメモリが割り当てられるため、プログラマは割り当てられたメモリの解放を忘れずに行う必要がある。

違反コード (Windows)

以下のコード例は、正規化に Windows 関数の GetFullPathName() を使用する [MSDN]。

/* ... */

enum { INITBUFSIZE = 256 };
DWORD ret = 0;
DWORD new_ret = 0;
char *canonical_filename;
char *new_file;
char *file_name;

/* ... */

file_name = (char *)malloc(strlen(argv[1])+1);
canonical_filename = (char *)malloc(INITBUFSIZE);

if ( (file_name != NULL) && (canonical_filename != NULL) ) {
  strcpy(file_name, argv[1]);
  strcpy(canonical_filename, "");
} else {
  /* エラー処理 */
}

ret = GetFullPathName(
  file_name,
  INITBUFSIZE,
  canonical_filename,
  NULL
);

if (ret == 0) {
  /* エラー処理 */
}
else if (ret > INITBUFSIZE) {
  new_file = (char *)realloc(canonical_filename, ret);
  if (new_file == NULL) {
    /* エラー処理 */
  }

  canonical_filename = new_file;

  new_ret = GetFullPathName(
    file_name,
    ret,
    canonical_filename,
    NULL
  );
  if (new_ret > ret) {
    /*
     * 2 回目の GetFullPathName() の呼び出しまでの間にパスの長さが変わった。
     * エラー処理
     */
  }
  else if (new_ret == 0) {
    /* エラー処理 */
  }
}

if (!verify_file(canonical_filename) {
  /* エラー処理 */
}
/* 使用前にファイル名を確認する */

GetFullPathName() 関数を使用して、パス名から .././ を除去できるが、UNC 共有、8.3 形式の短い名前、長い名前、Unicode 名、末尾のドット、フォワードスラッシュ、バックスラッシュ、ショートカットなど、GetFullPathName() では対応できない正規化の問題がほかにも多数ある。

また、GetFullPathName() を使用してファイル名を検査する場合にも、TOCTOU 状態を避けるように注意する必要がある。

適合コード (Windows)

Windows オペレーティングシステム用の正規化ファイル名の生成は非常に複雑であるため、このコーディングスタンダードでは説明しない。パス、ディレクトリ、ファイル名に基づいた判断を避けることが最善の解決法である [Howard 2002]。または、アクセス制御リスト(ACL)や他の認証方法など、オペレーティングシステム固有の仕組みを使用せよ。

リスク評価

ファイル関連の脆弱性はしばしば、昇格した権限のプログラムに意図しないファイルにアクセスさせるために悪用される。ファイルパスを正規化することで、参照するファイルオブジェクトを簡単に識別できる。

レコメンデーション

深刻度

可能性

修正コスト

優先度

レベル

FIO02-C

P8

L2

自動検出

ツール

バージョン

チェッカー

説明

Compass/ROSE

 

 

open() または fopen() の呼び出しの前に、正規化ルーチン、すなわち realpath() または canonicalize_file_name() の呼び出しが行われていることを確認することで、このレコメンデーションの違反を検出することができる。ただし ROSE は正規化がいつ保証されるのかを判断できないため、誤検知を引き起こす可能性がある。ファイル名文字列を対象にほかの処理を行っている場合にのみ、fopen() または open() の呼び出しを報告することで、誤検知を減らすことができる(なくすことはできない)。これは、ファイル名文字列に基づく検証を行うためだけに正規化が必要だからである。

Klocwork

V. 9.1

SV.CUDS.MISSING_ABSOLUTE_PATH

 

LDRA tool suite

V. 8.5.4

85 D

実装済み

関連する脆弱性

CVE-2009-1760 はこのレコメンデーションへの違反の結果である。libtorrent は、バージョン 0.4.13 までは、安全でないファイルパスの除去を試みる際に、".." 文字列についてのみ照合を行う。攻撃者はこれを悪用し、より複雑な相対パスを使用することでシステム上の任意のファイルにアクセスできる [xorl 2009]。

関連するガイドライン
CERT C++ Secure Coding Standard FIO02-CPP.信頼できない情報源から取得したファイル名は正規化する
CERT Oracle Secure Coding Standard for Java IDS02-J. パス名は検証する前に正規化する
ISO/IEC TR 24772:2013 Path Traversal [EWR]
MITRE CWE CWE-22, Path traversal
CWE-41, Failure to resolve path equivalence
CWE-59, Failure to resolve links before file access (aka "link following")
CWE-73, External control of file name or path
参考資料
[Austin Group 2008] realpath()
[Drepper 2006] Section 2.1.2, "Implicit Memory Allocation"
[Howard 2002] Chapter 11, "Canonical Representation Issues"
[Linux 2008] realpath(3)
pathconf(3)
[MSDN] "GetFullPathName Function"
[Open Group 2004] Section 4.11, "Pathname Resolution"
realpath()
[Seacord 2013] Chapter 8, "File I/O"
[VU#743092]  
[xorl 2009] CVE-2009-1760: libtorrent Arbitrary File Overwrite
翻訳元

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

FIO02-C. Canonicalize path names originating from untrusted sources (revision 248)

Top へ

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