閉じる

【C言語】tmpnamは使うな!危険性・脆弱性と安全な実装パターン大全

C言語で一時ファイルを扱う際、古くからtmpnam関数が利用されてきました。

しかし近年のセキュリティ要件やマルチユーザ環境、クラウド基盤では、tmpnamを使い続けることは深刻な脆弱性につながりかねません。

本記事では、tmpnamがなぜ危険なのかを具体例とともに解説し、安全な代替API(mkstemp、tmpfileなど)や既存コードの置き換え方法まで丁寧に整理していきます。

C言語のtmpnamとは

tmpnamとは何をする関数か

tmpnamは、C言語標準ライブラリで提供される一時ファイル名を生成するための関数です。

ポイントは、ファイル名の文字列を返すだけで、実際のファイルは作成しないという点です。

開発者は、この名前を使ってfopenなどの関数でファイルを作成します。

一般的には次のような流れで利用されます。

  1. tmpnamを呼び出して一時ファイル名の候補を取得する
  2. 取得したパスをfopenなどでオープンし、一時ファイルを作成する
  3. 処理が終わったらファイルを閉じて削除する

このように、一時ファイルのための「名前だけ」を用意する役割を持っている関数です。

基本的な使用例

まず、教科書的なtmpnamの使い方を見てみます。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char filename[L_tmpnam];          // 一時ファイル名を格納するバッファ
    char *ret;

    // 一時ファイル名の生成
    ret = tmpnam(filename);
    if (ret == NULL) {
        perror("tmpnam failed");
        return EXIT_FAILURE;
    }

    printf("tmpnam returned: %s\n", filename);

    // 生成された名前を使ってファイルを作成
    FILE *fp = fopen(filename, "w+"); // 読み書きできる一時ファイルとして開く
    if (fp == NULL) {
        perror("fopen failed");
        return EXIT_FAILURE;
    }

    fputs("temporary data\n", fp);
    rewind(fp);

    char buf[128];
    if (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("read from temp file: %s", buf);
    }

    // ファイルを閉じて削除
    fclose(fp);
    if (remove(filename) != 0) {
        perror("remove failed");
    }

    return EXIT_SUCCESS;
}
実行結果
例) 実行結果イメージ
tmpnam returned: /tmp/filelRk2a9
read from temp file: temporary data

このコードを見ると「便利そうだ」と感じるかもしれません。

しかし、この使い方には後述する重大な問題が潜んでいます。

C言語標準でのtmpnamの位置付けと仕様

C言語標準(ISO C)では、tmpnamはstdio.hに宣言される入出力関連のユーティリティ関数として定義されています。

仕様上のポイントを整理すると次のようになります。

  • 関数プロトタイプ
    char *tmpnam(char *s);
    引数sNULLのときは、内部静的領域を使って名前を返します。非NULLのときは、呼び出し側が用意したバッファに結果を書き込みます。
  • 戻り値
    正常時には一時ファイル名を指すポインタ(引数s、または内部バッファ)を返し、失敗した場合はNULLを返します。
  • 有効なバッファサイズ
    L_tmpnamというマクロが一時ファイル名を格納するのに十分なサイズとして定義されています。
  • ファイルは作られない
    tmpnamはファイルを作成せず、あくまで「将来有効かもしれないパス名」を返すだけです。この点が後述する脆弱性の原因となります。

ISO Cの仕様では、tmpnamに対して「セキュアである」という保証は行っていません。

むしろ標準の中で、tmpnamを使用する場合は注意が必要であることが明記されています。

なぜtmpnamは「非推奨」とされるのか

多くの教科書やリファレンスで、tmpnamは「非推奨(deprecated)」あるいは「安全ではない」とされています。

その理由を整理すると、次のような観点に集約されます。

  1. レースコンディションが必然的に発生する設計
    tmpnamは「ファイル名を返すだけ」で、名前の生成とファイルの作成が別々の操作になっています。この隙間の時間に他プロセスや攻撃者が同じ名前でファイルを作成する余地が生まれ、乗っ取りなどを許してしまいます。
  2. 予測可能なパターンによる情報漏えい・なりすまし
    実装によっては、一時ファイル名のパターンが単純で、他ユーザから推測可能になります。その結果、ファイル名を推測して中身を盗み見られたり、意図しないファイル操作を誘発したりします。
  3. マルチスレッドやマルチプロセス環境への不適合
    内部バッファの共有やスレッドセーフでない実装が多く、現代的な環境では簡単に不具合を引き起こす可能性があります。
  4. セキュリティガイドラインで明確に禁止されている
    CERT CやOWASPなど、著名なガイドラインがtmpnamを危険APIとして挙げており、利用は避けるべきとしています。

こうした理由から、新規コードでtmpnamを使うことは原則として禁物であり、既存コードでも計画的に置き換えていく必要があります。

tmpnamの危険性・脆弱性

競合状態(race condition)による一時ファイル乗っ取り

tmpnamの最も深刻な問題は、競合状態(race condition)による一時ファイル乗っ取りです。

tmpnamが返すのは「まだ存在しないはずのファイル名」です。

しかし、その後fopenでファイルを開くまでには、わずかとはいえ時間差があります。

この間に攻撃者が同じ名前でファイル(あるいはシンボリックリンク)を作成すると、プログラムは攻撃者が用意したファイルを「一時ファイル」と信じて操作してしまう可能性があります。

脆弱なコード例

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char filename[L_tmpnam];

    // 一時ファイル名を取得
    if (tmpnam(filename) == NULL) {
        perror("tmpnam failed");
        return EXIT_FAILURE;
    }

    printf("Using temp file: %s\n", filename);

    // レースコンディションのあるファイル作成
    FILE *fp = fopen(filename, "w+"); // ← ここまでの間に、他プロセスが同名ファイルを作成可能
    if (fp == NULL) {
        perror("fopen failed");
        return EXIT_FAILURE;
    }

    fputs("secret\n", fp);
    fclose(fp);

    return EXIT_SUCCESS;
}

このコードでは、tmpnamとfopenの間に「名前が空いていることを確認してから、その名前でファイルを作成する」という2段階の操作があります。

OSレベルでは、これは1つの不可分操作ではありません。

攻撃者がこの時間差を利用し、同じ名前でシンボリックリンクを作成し、システム上の別の重要ファイル(例: 設定ファイル)に向けておけば、プログラムは意図せず重要ファイルを上書きしてしまうことになります。

予測可能なファイル名と情報漏えいリスク

多くのtmpnam実装は、歴史的な理由から単純な連番や時刻、プロセスIDなどを組み合わせて一時ファイル名を生成してきました。

これ自体は標準規格に違反しませんが、攻撃者から見て非常に推測しやすいという問題があります。

予測可能な一時ファイル名は、次のようなリスクを生みます。

  • 他ユーザが一時ファイルの存在と中身を推測しやすくなり、覗き見を許す
  • 同じ名前を先に作成しておくことで、意図しないファイルへのアクセスを誘発できる
  • 分散環境やクラウド環境では、一時ファイル名が情報漏えいの足掛かりになる

特に、ログインIDや機密データを一時ファイルに書き出している場合、その存在自体が攻撃者にとって貴重な情報源になります。

一時ファイルのパーミッション問題

tmpnam自体はファイルを作成しませんが、一般的にはfopenなどと組み合わせて一時ファイルを作成します。

このとき、パーミッション(アクセス権)が安全とは限らないという問題があります。

多くのUNIX系システムでは、fopenで新規ファイルを作成するときのモードは0666 & ~umaskに依存します。

つまり、プロセスのumask設定によっては、他のユーザから読み取り可能な一時ファイルが作成されてしまう可能性があります。

一方で、安全な一時ファイル作成API(後述のmkstempなど)は、明示的に0600(所有者のみ読み書き可能)でファイルを作成するよう設計されていることが多いです。

この違いが、機密情報の漏えいリスクに直結します。

マルチスレッド環境での安全性の欠如

tmpnamの仕様には、スレッド安全であることに関する保証がありません

特にtmpnam(NULL)のように呼び出した場合、内部の静的バッファが共有されるため、マルチスレッド環境では次のような問題が発生しうるとされています。

  • 別スレッドからの呼び出しが同じバッファを書き換えてしまう
  • 呼び出し元が保持しているポインタが、意図せず別の名前に変わってしまう
  • スレッド間で一時ファイル名が衝突し、データ破壊や例外を引き起こす

このため、マルチスレッド対応が前提の現代的なアプリケーションでは、tmpnamを使う設計自体が問題だといえます。

セキュリティガイドライン(CERT/OWASPなど)がtmpnamを禁止する理由

代表的なセキュリティガイドラインでは、tmpnamは明確に「使用を避けるべき関数」として挙げられています。

  • CERT C コーディングスタンダード
    一時ファイルに関するルールで、tmpnamtempnamの使用を非推奨とし、代わりにmkstemptmpfileなどの安全なAPIを用いることを推奨しています。
  • OWASP/CWE
    一時ファイルの不適切な扱いは、CWE-377(不適切な一時ファイル)やCWE-59(リンクの不適切な処理)などとして整理され、一時ファイル名を先に生成してからファイルを開くパターンが危険であるとされています。

これらのガイドラインが共通して指摘するのは、tmpnamの設計そのものが現代のセキュリティ要件と相容れないという点です。

tmpnamの代替・安全な実装パターン

mkstempによる安全な一時ファイル作成

POSIX環境(UNIX/Linux系)では、mkstempが一時ファイル作成のデファクトスタンダードです。

mkstempの特徴は、次の2点に集約されます。

  1. ファイル名の生成とファイル作成を「一度に」行う
    内部的にはO_CREAT | O_EXCLフラグを使って、存在しない名前のファイルだけを新規作成します。これにより、tmpnamで問題だったレースコンディションが解消されます。
  2. 安全なパーミッション(通常0600)で作成する
    他ユーザから読めないよう、所有者のみ読み書き可能なモードでファイルが作成されます。

mkstempの基本的な使い方

C言語
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>   // mkstemp
#include <string.h>   // strcpy など

int main(void) {
    // テンプレート文字列を準備 (末尾の XXXXXX が必要)
    char template[] = "/tmp/myappXXXXXX";

    // mkstemp は template を直接書き換えて、実際のパスにする
    int fd = mkstemp(template);
    if (fd == -1) {
        perror("mkstemp failed");
        return EXIT_FAILURE;
    }

    printf("created secure temp file: %s\n", template);

    // fd から FILE* に変換したい場合は fdopen を使う
    FILE *fp = fdopen(fd, "w+");
    if (fp == NULL) {
        perror("fdopen failed");
        close(fd);    // 失敗時は必ずclose
        return EXIT_FAILURE;
    }

    fputs("secure temporary data\n", fp);
    rewind(fp);

    char buf[128];
    if (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("read from temp file: %s", buf);
    }

    // 使用後に削除してから閉じる
    if (remove(template) != 0) {
        perror("remove failed");
    }
    fclose(fp); // fd も同時に閉じられる

    return EXIT_SUCCESS;
}
実行結果
例) 実行結果イメージ
created secure temp file: /tmp/myappk5T7cD
read from temp file: secure temporary data

このように、mkstempはファイルディスクリプタと実際のパスの両方を安全に取得できるため、tmpnamの完全な上位互換と言えます。

tmpfileによる自動削除される一時ファイル

C標準ライブラリには、tmpfileという一時ファイルを直接作成してくれる関数があります。

大きな特徴は、ファイル名を意識せずに使えることと、クローズ時やプロセス終了時に自動削除されることです。

tmpfileの基本的な使い方

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 匿名一時ファイルを生成
    FILE *fp = tmpfile();
    if (fp == NULL) {
        perror("tmpfile failed");
        return EXIT_FAILURE;
    }

    // 通常のファイルと同様に入出力できる
    fputs("data in tmpfile\n", fp);
    rewind(fp);

    char buf[128];
    if (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("read: %s", buf);
    }

    // fcloseすると、一時ファイルは自動的に削除される
    fclose(fp);

    // 明示的な remove は不要
    return EXIT_SUCCESS;
}
実行結果
例) 実行結果イメージ
read: data in tmpfile

tmpfileはファイル名を第三者から隠蔽できるため、他プロセスからの覗き見や競合のリスクが低いという利点があります。

一方で、ファイル名が必要となる場面(他プロセスとファイルを共有する、など)には向きません。

その場合はmkstempを使うのが適切です。

POSIX環境でのmkdtempとディレクトリ運用

POSIX環境では、mkdtempを使って安全な一時ディレクトリを作成し、その中で一時ファイルを運用するパターンがよく使われます。

このアプローチには次のメリットがあります。

  • ディレクトリ単位でパーミッションを制御できる(通常0700)
  • アプリケーションごとに独立した一時領域を確保できる
  • ディレクトリの削除でまとめてクリーンアップできる

mkdtempの利用例

C言語
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>   // mkdtemp
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(void) {
    char tmpl[] = "/tmp/myappdirXXXXXX";

    // 一時ディレクトリを安全に作成
    char *dir = mkdtemp(tmpl);
    if (dir == NULL) {
        perror("mkdtemp failed");
        return EXIT_FAILURE;
    }

    printf("created temp directory: %s\n", dir);

    // その中に一時ファイルを作る例 (単純化のため安全性は mkstemp に依存)
    char filepath[256];
    snprintf(filepath, sizeof(filepath), "%s/tmpfileXXXXXX", dir);

    int fd = mkstemp(filepath);
    if (fd == -1) {
        perror("mkstemp failed");
        // 失敗してもディレクトリは削除したほうがよい
        rmdir(dir);
        return EXIT_FAILURE;
    }

    printf("created temp file: %s\n", filepath);

    FILE *fp = fdopen(fd, "w+");
    if (fp == NULL) {
        perror("fdopen failed");
        close(fd);
        unlink(filepath);
        rmdir(dir);
        return EXIT_FAILURE;
    }

    fputs("data in per-app temp dir\n", fp);
    fclose(fp);

    // クリーンアップ (順序に注意: まずファイル、次にディレクトリ)
    if (unlink(filepath) != 0) {
        perror("unlink failed");
    }
    if (rmdir(dir) != 0) {
        perror("rmdir failed");
    }

    return EXIT_SUCCESS;
}
実行結果
例) 実行結果イメージ
created temp directory: /tmp/myappdircV1a8Q
created temp file: /tmp/myappdircV1a8Q/tmpfilepL9y6M

このように、一時ディレクトリ + mkstempの組み合わせは、セキュアで管理しやすい一時領域を提供します。

Windows環境での_secure_tmpnamなどプラットフォーム別対策

Windows環境では、標準Cのtmpnamのほかに、Microsoft独自拡張として_tmpnam_s_mktemp_sなどの「セキュアバージョン」が提供されています。

ただし、これらも根本的な設計はtmpnamと似ており、名前生成と作成が分離している点で完全に安全とは言えません。

Windowsで可能な対策は次のようになります。

  • 可能であればtmpfileを優先する
  • ファイル名が必要な場合でも、一時ディレクトリ(例: GetTempPath + GetTempFileName)と組み合わせ、適切なアクセス制御付きでファイルを生成する
  • Microsoft拡張の_s系関数を使う場合も、race conditionに注意する

Windows固有APIを使うとややコードは増えますが、セキュリティポリシーやACLを踏まえた一時ファイル管理が必要になります。

一時ファイルディレクトリ(TMPDIRなど)の安全な扱い方

一時ファイルの安全性は、どのディレクトリに作るかにも大きく依存します。

多くのシステムでは、環境変数TMPDIRTEMPTMPを使って一時ディレクトリを指定しますが、次のような点に注意が必要です。

  • 世界書き込み可能(0777)なディレクトリでは、攻撃者によるシンボリックリンク攻撃などが起きやすい
  • スティッキービット(t)が設定されているか(/tmp など)を確認し、他ユーザによる勝手な削除を防ぐ
  • 可能であればユーザ専用の一時ディレクトリをホームディレクトリ配下などに用意し、0700で保護する

一時ファイルを安全に扱うには、APIだけでなくディレクトリのパーミッションや環境変数の設定も含めて設計することが重要です。

乱数ベースの自前実装を避けるべき理由

tmpnamの危険性を認識した開発者の中には、「それなら自分で乱数を使って一時ファイル名を作ればいい」と考える方もいます。

しかし、乱数ベースの自前実装はほぼ確実に危険です。

主な理由は次の通りです。

  • rand()などの一般的な疑似乱数は予測が容易であり、攻撃者がパターンを逆算できる
  • 名前生成とファイル作成を分離する限り、race conditionの問題は解決しない
  • 衝突検出や再試行ロジックを自前で書くと、エッジケースでのバグやDoSの原因になりやすい

一時ファイルの安全な作成は、OSや標準ライブラリが提供するAPIに任せるのが原則です。

自前実装は避けるべきです。

既存コードからtmpnamを置き換える実践手順

典型的なtmpnam使用パターンの洗い出し

既存コードからtmpnamを除去するには、まずどのような使い方をしているかを洗い出す必要があります。

典型的には次のようなパターンが見られます。

  1. tmpnam → fopen → fclose → remove
    最も一般的なパターンで、一時ファイルを作って読み書きし、最後に削除するケースです。多くの場合、mkstempへの置き換えが適しています。
  2. tmpnamで作ったファイル名を別プロセスに渡す
    他プロセスとの通信や外部ツールとの連携に一時ファイルを使っているケースです。この場合も、名前付きの安全なファイルが必要なので、mkstemp + パス共有が候補になります。
  3. ファイル名だけを必要とし、プロセス内でしか使わない
    ログ的な用途や一時的なバッファとして使っている場合です。この場合はtmpfileに置き換えられる可能性が高いです。

こうしてパターンを分類することで、どの代替APIが最適かを判断しやすくなります。

tmpnamからmkstempへの書き換え手順

典型的なtmpnam + fopenパターンを、mkstempに置き換える手順を具体的なコードで示します。

変換前のコード(脆弱)

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char filename[L_tmpnam];

    if (tmpnam(filename) == NULL) {
        perror("tmpnam failed");
        return EXIT_FAILURE;
    }

    FILE *fp = fopen(filename, "w+");
    if (fp == NULL) {
        perror("fopen failed");
        return EXIT_FAILURE;
    }

    /* ... 一時ファイルを利用 ... */

    fclose(fp);
    remove(filename);

    return EXIT_SUCCESS;
}

変換後のコード(mkstemp利用)

C言語
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>   // mkstemp
#include <string.h>

int main(void) {
    // 一時ファイル名テンプレート (末尾6文字は X)
    char template[] = "/tmp/myappXXXXXX";

    // ファイルを安全に作成し、fdと実際の名前を得る
    int fd = mkstemp(template);
    if (fd == -1) {
        perror("mkstemp failed");
        return EXIT_FAILURE;
    }

    // 必要に応じて FILE* に変換
    FILE *fp = fdopen(fd, "w+");
    if (fp == NULL) {
        perror("fdopen failed");
        close(fd);    // fdopen失敗時のクリーンアップ
        unlink(template);
        return EXIT_FAILURE;
    }

    /* ... 一時ファイルを利用 ... */

    // 使用後に削除してから閉じる (順序はユースケースに応じて)
    if (remove(template) != 0) {
        perror("remove failed");
        // ここでエラーでも、少なくとも fp は閉じる
    }
    fclose(fp);

    return EXIT_SUCCESS;
}

ポイントは、tmpnamでの「名前生成」とfopenでの「ファイル作成」を、mkstempで一体化することです。

これにより、レースコンディションの余地がなくなります。

tmpnamからtmpfileへの移行パターン

もし既存コードが一時ファイル名そのものを外部に公開していないのであれば、よりシンプルで安全なtmpfileへの移行を検討できます。

変換前の例

C言語
char filename[L_tmpnam];
FILE *fp;

tmpnam(filename);
fp = fopen(filename, "w+");

/* プロセス内でのみ使用し、他プロセスとの共有はなし */

fclose(fp);
remove(filename);

変換後の例(tmpfile)

C言語
FILE *fp;

fp = tmpfile();
if (fp == NULL) {
    perror("tmpfile failed");
    /* エラー処理 */
}

/* プロセス内でのみ使用し、他プロセスとの共有も不要 */

fclose(fp); // 自動的に削除される

このように、ファイル名に依存していないロジックであれば、tmpfileへの置き換えは比較的容易です。

クリーンアップもfcloseひとつで済むため、実装の簡素化にもつながります。

権限・クリーンアップ処理の見直しポイント

tmpnamからの置き換えでは、権限設定とクリーンアップ処理を見直す良い機会でもあります。

次のような観点でコードを見直します。

  • ファイル作成時のパーミッション
    mkstempやtmpfileは比較的安全なモードで作成しますが、必要に応じてchmodで明示的に制御することも検討します。
  • 例外パスでのリソースリーク
    途中でエラーが起きた場合にファイルハンドルや一時ファイルが残り続けないかを確認します。gotoによる一括クリーンアップパターンなども有効です。
  • 削除のタイミング
    処理途中で一時ファイルを削除(アンリンク)し、名前を誰からも見えなくしておくことで、セキュリティをさらに高めることができます。

レビューで見るべきC言語一時ファイル処理のチェックリスト

コードレビュー時には、次のような観点で一時ファイル処理をチェックするとよいです。

  • 危険APIの使用有無
    tmpnam, tempnam, mktempなど、危険とされている関数が使われていないかを確認します。
  • 名前生成とファイル作成の分離
    どの関数を使っていても、「名前だけ作る」→「後でopen/fopenする」というパターンがあれば要注意です。
  • パーミッションとディレクトリ
    ファイルモードが意図どおりか、ディレクトリが世界書き込み可になっていないか、環境変数の影響を受け過ぎていないかを確認します。
  • クリーンアップの網羅性
    正常終了・異常終了を問わず、ファイルハンドルと一時ファイルが必ず解放・削除されることを確認します。
  • マルチスレッド/マルチプロセス環境での挙動
    スレッドセーフでないAPIや共有バッファの使用がないか、競合の可能性がないかを確認します。

これらをチェックリストとして運用することで、tmpnamに限らず一時ファイル全般のセキュリティ品質を高めることができます。

まとめ

tmpnamは、かつては便利な一時ファイル名生成関数として広く利用されていましたが、レースコンディションや予測可能な名前、パーミッション問題など、現代の環境では看過できない脆弱性を抱えています。

CERTやOWASPも使用を避けるよう明確に勧告しており、新規コードでの利用はもちろん、既存コードについてもmkstempやtmpfileなどの安全なAPIへの置き換えが必須です。

本記事で紹介した代替パターンやレビュー観点を参考にしながら、一時ファイル処理のセキュリティと品質を体系的に見直していくことをおすすめします。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!