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

FIO15-C. ファイル操作はセキュアディレクトリで行う

ファイル操作は セキュアなディレクトリ で行うべきである。セキュアなディレクトリとは、ユーザ本人以外または場合によっては管理者以外は誰も、ファイルの作成、名前変更、削除などのファイル操作ができないディレクトリを指す。(他のユーザはディレクトリ内で読み取りや検索はできるが、一般にどのような方法でも、ディレクトリの内容を変更することはできない。) さらに、セキュアなディレクトリの親ディレクトリとそれより上のすべてのディレクトリにおいて、他のユーザは、自分が所有するファイルの新規作成、削除およびファイル名の変更はできるが、それ以外のファイルを削除したりファイル名を変更したりすることはできない。

セキュアなディレクトリ内でファイルを操作すれば、攻撃者がファイルやファイルシステムを改ざんする可能性をなくすことができる。この種の脆弱性は、ファイル名と実際のファイルコンテンツの紐付けが緩いために発生することが多い(「FIO01-C. ファイル名を使用してファイルを識別する関数の使用に注意する」を参照)。場合によっては、ファイル操作を任意の場所で安全に実行できることもあるが、通常、安全なファイル操作を保証する唯一の方法は、操作をセキュアなディレクトリ内で行うことである。

ファイルシステムが安全な方法で設定されていることを確認するのは、典型的にはシステム管理者の役割である。しかし多くの場合、プログラムは、セキュリティ上の脆弱性が引き起こされるおそれのあるファイル操作を行う前に、ファイルシステムが安全に設定されているかどうかを検査すべきである。プロセスの実行中や検査後に、セキュリティが損なわれるような形にファイルシステムが設定される可能性はある。そのため、セキュアなディレクトリで操作を行う場合でも、コードを安全な方法で実装することを推奨する(つまり、このセクションに記載の他のルールやレコメンデーションに従うこと)。

違反コード

以下のコード例では、file_name で識別されるファイルをオープンして処理し、閉じ、削除している。

char *file_name;
FILE *fp;

/* file_name を初期化 */

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

/* ... ファイルの処理 ... */

if (fclose(fp) != 0) {
  /* エラー処理 */
}

if (remove(file_name) != 0) {
  /* エラー処理 */
}

攻撃者は fopen() を呼び出す前に、 file_name によって識別されるファイルオブジェクトを任意のファイルへのリンクに置き換えるかもしれない。また、remove() の呼び出しにおいて file_name で識別されるファイルオブジェクトが、fopen() の呼び出しにおいて file_name で識別されるファイルとは異なる可能性もある。たとえば /tmp/app/tmpdir/passwd など、ファイルがセキュアなディレクトリにない場合、攻撃者はファイルの場所を以下のように操作することができる。

% cd /tmp/app/
% rm -rf tmpdir
% ln -s /etc tmpdir

信頼できないユーザによるファイルの操作を防ぐためにアクセス権限を適切に設定したセキュアディレクトリでファイルをオープンする以外に、実際に処理を行ったファイルの削除をプログラムで確実に行う方法はない。

適合コード (POSIX)

引数で与えられたディレクトリ fullpath とそれより上のすべてのディレクトリがユーザまたはスーパーユーザによって所有されており、他のユーザが削除や名前変更を行えないことを検査する secure_dir() 関数のサンプル実装を以下に示す。ディレクトリを検査する場合、危険な競合状態を回避するためにルートから末端のディレクトリへという順序で検査することが重要である。この順序を守らないと、いずれかのディレクトリに対する権限を持つ攻撃者が、権限が検証された後でディレクトリ名を変更し、ディレクトリを作り変えてしまう可能性がある。

fullpath を正規化する必要はない(「FIO02-C. 汚染された情報源から取得したパス名は正規化する」を参照)。パスにシンボリックリンクが含まれている場合には、リンク先のディレクトリに対して自身を再帰的に呼び出し、それが安全であることを検査する。シンボリックリンクされたディレクトリは、リンク元とリンク先のディレクトリの両方が安全である場合に安全であるといえる。

この関数は UNIX のファイルパーミッションと互換性のある仕組みを持ったファイルシステムでのみ有効であることに注意。AFS (Andrew File System) のような、異なるパーミッションの仕組みを持つファイルシステム上では正しく動作しないだろう。

#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <libgen.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

enum { MAX_SYMLINKS = 5 };

/* ディレクトリがセキュアならばゼロ以外の値、それ以外の場合はゼロを返す */
int secure_dir(const char *fullpath) {
  static unsigned int num_symlinks = 0;
  char *path_copy = NULL;
  char **dirs = NULL;
  int num_of_dirs = 1;
  int secure = 1;
  int i, r;
  struct stat buf;
  uid_t my_uid = geteuid();
  size_t linksize;
  char* link;

  if (!fullpath || fullpath[0] != '/') {
    /* エラー処理 */
  }

  if (num_symlinks > MAX_SYMLINKS) {  /* シンボリックリンクがループしていると判定 */
    /* エラー処理 */
  }
 
  if (!(path_copy = strdup(fullpath))) {
    /* エラー処理 */
  }

  /* root からの深さを計算 */
  char* path_parent = path_copy;
  for (; ((strcmp(path_parent, "/") != 0) &&
          (strcmp(path_parent, "//") != 0) &&
          (strcmp(path_parent, ".") != 0));
       path_parent = dirname(path_parent)) {
    num_of_dirs++;
  } /* num_of_dirs に検査すべきディレクトリの数がはいっている */
  free(path_copy);
  path_copy = NULL;

  if (!(dirs = (char **)malloc(num_of_dirs * sizeof(char *)))) {
    /* エラー処理 */
  }

  if (!(dirs[num_of_dirs - 1] = strdup(fullpath))) {
    /* エラー処理 */
  }

  if (!(path_copy = strdup(fullpath))) {
    /* エラー処理 */
  }

  /* ここで dirs 配列を埋める */
  path_parent = path_copy;
  for (i = num_of_dirs - 2; i >= 0; i--) {
    path_parent = dirname(path_parent);
    if (!(dirs[i] = strdup(path_parent))) {
      /* エラー処理 */
    }
  }
  free(path_copy);
  path_copy = NULL;

  /*
   * ルートから fullpath までたどりながら、
   * パーミッションを検査する
   */
  for (i = 0; i < num_of_dirs; i++) {
    if (lstat(dirs[i], &buf) != 0) {
      /* エラー処理 */
    }

    if (S_ISLNK(buf.st_mode)) { /* Symlink。リンクが指すファイルを判定 */
      linksize = buf.st_size + 1;
      if (!(link = (char *)malloc(linksize))) {
        /* エラー処理 */
      }

      r = readlink(dirs[i], link, linksize);
      if (r == -1) {
        /* エラー処理 */
      } else if (r >= linksize) {
        /* 切り捨てエラーの処理 */
      }
      link[r] = '\0';

      num_symlinks++;
      r = secure_dir(link);
      num_symlinks--;

      if (!r) {
        secure = 0;

        free(link);
        link = NULL;
        break;
      }

      free(link);
      link = NULL;

      continue;
    }

    if (!S_ISDIR(buf.st_mode)) { /* ディレクトリでない */
      secure = 0;
      break;
    }

    if ((buf.st_uid != my_uid) && (buf.st_uid != 0)) {
      /* ディレクトリが他のユーザによって所有されている */
      secure = 0;
      break;
    }

    if (buf.st_mode & (S_IWGRP | S_IWOTH)) { /* ディレクトリは他のユーザによる書き込みが可能 */
      secure = 0;
      break;
    }
  }

  for (i = 0; i < num_of_dirs; i++) {
    free(dirs[i]);
    dirs[i] = NULL;
  }

  free(dirs);
  dirs = NULL;

  return secure;
}

以下の適合コードでは、ファイルをオープンして処理を行い、その後ファイルを削除している。処理を始める前に secure_dir() 関数を使って攻撃者に干渉されないことを確認している。ディレクトリのパス名を secure_dir() で検査した後、それ以降のファイル操作はすべて同じパスを使用して実行しなければならない。

char *dir_name;
const char *file_name = "passwd"; /* セキュアディレクトリ内のファイル名 */
FILE *fp;

/* dir_name を初期化 */

if (!secure_dir(dir_name)) {
  /* エラー処理 */
}

if (chdir(dir_name) == -1) {
  /* エラー処理 */
}

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

/* ... ファイルの処理 ... */

if (fclose(fp) != 0) {
  /* エラー処理 */
}

if (remove(file_name) != 0) {
  /* エラー処理 */
}
リスク評価

ファイル入出力操作をセキュアディレクトリで行わないと、ファイルシステムに関連するさまざまな脆弱性が引き起こされるおそれがある。

レコメンデーション

深刻度

可能性

修正コスト

優先度

レベル

FIO15-C

P4

L3

Related Guidelines
CERT C++ Secure Coding Standard FIO15-CPP. Ensure that file operations are performed in a secure directory
MITRE CWE CWE-379, Creation of temporary file in directory with insecure permissions
CWE-552, Files or directories accessible to external parties
Bibliography
[IEEE Std 1003.1:2013] XSH, System Interfaces, dirname
XSH, System Interfaces, realpath
[Viega 2003] Section 2.4, "Determining Whether a Directory Is Secure"
翻訳元

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

FIO15-C. Ensure that file operations are performed in a secure directory (revision 61)

Top へ

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