C# Tips | ファイル・ディレクトリ操作:パス安全化

C sharp C#
スポンサーリンク

はじめに 「パス安全化」は“ファイルを触る前の身だしなみ”

業務でファイルを扱うコードを書くとき、本当によく出てくるのが「パス文字列」です。
ユーザー入力、設定ファイル、外部システムから渡される値――それらをそのまま File.OpenDirectory.CreateDirectory に渡すと、例外になったり、最悪「触ってはいけない場所」にアクセスしてしまうことがあります。

そこで必要になるのが「パス安全化(サニタイズ)」です。
ざっくり言うと、

  • OS 的に不正な文字を取り除く(または置き換える)
  • 相対パスを正規化して、「どこを指しているか」をはっきりさせる
  • 想定外の場所(上位ディレクトリなど)に出ていかないようにチェックする

といったことを、ファイル操作の前にきちんとやる、という考え方です。

ここでは、C# 初心者向けに、「パス安全化」の基本と、実務でそのまま使えるユーティリティ例を、かみ砕いて説明していきます。


不正な文字を取り除く・置き換える

なぜ「そのままの文字列」では危ないのか

Windows には、「ファイル名やパスに使ってはいけない文字」が決まっています。
例えば *?:"<>| などです。
こういった文字が含まれていると、File.CreateDirectory.CreateDirectoryNotSupportedException が飛びます。

ユーザー入力や外部データからパスを組み立てるときは、まず「その文字列が OS 的に有効か」をチェックし、ダメなら削る・置き換える必要があります。

Path.GetInvalidFileNameChars / GetInvalidPathChars を使う

.NET には、「ファイル名/パスに使えない文字」を教えてくれるメソッドが用意されています。

using System;
using System.IO;
using System.Linq;

public static class PathSanitizer
{
    public static string SanitizeFileName(string fileName, char replacement = '_')
    {
        var invalid = Path.GetInvalidFileNameChars();

        var sanitizedChars = fileName
            .Select(ch => invalid.Contains(ch) ? replacement : ch)
            .ToArray();

        return new string(sanitizedChars);
    }

    public static string SanitizePath(string path, char replacement = '_')
    {
        var invalid = Path.GetInvalidPathChars();

        var sanitizedChars = path
            .Select(ch => invalid.Contains(ch) ? replacement : ch)
            .ToArray();

        return new string(sanitizedChars);
    }
}
C#

SanitizeFileName は「ファイル名部分(拡張子込み)」を対象に、
SanitizePath は「パス全体」を対象に、不正文字を指定の文字(デフォルトは _)に置き換えています。

ここでの重要ポイントは、「OS が教えてくれる“不正文字リスト”を使う」ということです。
自分で「たぶんこの辺がダメだろう」と決め打ちするのではなく、Path.GetInvalidFileNameChars / Path.GetInvalidPathChars を使うことで、環境依存の違いも吸収できます。

例:ユーザー入力から安全なファイル名を作る

例えば、ユーザーが入力したタイトルをファイル名にしたい、というケース。

string title = "売上レポート: 2025/01/28 *確定版*";

string safeFileName = PathSanitizer.SanitizeFileName(title) + ".txt";

Console.WriteLine(safeFileName);
// 例: 売上レポート_ 2025_01_28 _確定版_.txt
C#

:/* などが _ に置き換えられ、OS 的に有効なファイル名になります。


パスの正規化と「どこを指しているか」をはっきりさせる

Path.GetFullPath で絶対パスにする

相対パス("..\data\file.txt" など)のままだと、「最終的にどの場所を指しているのか」が分かりにくくなります。
そこで、Path.GetFullPath を使って「絶対パス」に正規化するのが基本です。

using System;
using System.IO;

public static class PathNormalizer
{
    public static string Normalize(string path, string? baseDirectory = null)
    {
        if (string.IsNullOrEmpty(baseDirectory))
        {
            return Path.GetFullPath(path);
        }

        string combined = Path.Combine(baseDirectory, path);
        return Path.GetFullPath(combined);
    }
}
C#

使い方のイメージです。

string baseDir = @"C:\app\data";
string relative = @"..\input\file.csv";

string fullPath = PathNormalizer.Normalize(relative, baseDir);

Console.WriteLine(fullPath);
// 例: C:\app\input\file.csv
C#

ここでのポイントは、「Path.CombinePath.GetFullPath」のセットで、
... を含む相対パスをきれいに解決していることです。

「許可されたルートからはみ出していないか」をチェックする

セキュリティ的に重要なのが、「想定しているルートフォルダの外に出ていないか」をチェックすることです。
例えば、「C:\app\data の下だけを触るはずなのに、..\..\Windows\system32 にアクセスされる」ようなことは絶対に避けたいわけです。

これを防ぐには、「正規化した絶対パスが、ルートフォルダの配下にあるか」を確認します。

using System;
using System.IO;

public static class PathSecurity
{
    public static string EnsureUnderRoot(string rootDirectory, string targetPath)
    {
        string rootFull = Path.GetFullPath(rootDirectory);
        string targetFull = Path.GetFullPath(targetPath);

        if (!targetFull.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("許可されていないパスです。");
        }

        return targetFull;
    }
}
C#

使い方です。

string root = @"C:\app\data";
string userInput = @"..\..\Windows\system32\evil.txt";

string combined = Path.Combine(root, userInput);

try
{
    string safePath = PathSecurity.EnsureUnderRoot(root, combined);
    Console.WriteLine($"安全なパス: {safePath}");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
    // 「許可されていないパスです。」と出る
}
C#

ここでの重要ポイントは、

  • まず Path.GetFullPath で両方を絶対パスにする
  • その上で StartsWith で「ルート配下かどうか」を判定する

という二段構えになっていることです。
相対パスのまま StartsWith("C:\\app\\data") などと比較しても、.. が含まれていると正しく判定できません。


「パス安全化ユーティリティ」をひとまとめにする

よく使う処理を 1 クラスにまとめる

ここまでの要素を組み合わせて、「業務でそのまま使える“パス安全化ユーティリティ”」を 1 クラスにまとめてみます。

using System;
using System.IO;
using System.Linq;

public static class SafePath
{
    public static string SanitizeFileName(string fileName, char replacement = '_')
    {
        var invalid = Path.GetInvalidFileNameChars();
        var chars = fileName
            .Select(ch => invalid.Contains(ch) ? replacement : ch)
            .ToArray();

        return new string(chars);
    }

    public static string SanitizePath(string path, char replacement = '_')
    {
        var invalid = Path.GetInvalidPathChars();
        var chars = path
            .Select(ch => invalid.Contains(ch) ? replacement : ch)
            .ToArray();

        return new string(chars);
    }

    public static string Normalize(string path, string? baseDirectory = null)
    {
        if (string.IsNullOrEmpty(baseDirectory))
        {
            return Path.GetFullPath(path);
        }

        string combined = Path.Combine(baseDirectory, path);
        return Path.GetFullPath(combined);
    }

    public static string EnsureUnderRoot(string rootDirectory, string targetPath)
    {
        string rootFull = Path.GetFullPath(rootDirectory);
        string targetFull = Path.GetFullPath(targetPath);

        if (!targetFull.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("許可されていないパスです。");
        }

        return targetFull;
    }

    public static string BuildSafePath(string rootDirectory, string unsafeRelativePath)
    {
        string sanitized = SanitizePath(unsafeRelativePath);
        string normalized = Normalize(sanitized, rootDirectory);
        return EnsureUnderRoot(rootDirectory, normalized);
    }
}
C#

BuildSafePath は、「ルートディレクトリ」と「怪しい相対パス」から、
「サニタイズ → 正規化 → ルート配下チェック」までを一気にやって、安全な絶対パスを返すメソッドです。

例:アップロードされたファイルを安全な場所に保存する

例えば、Web アプリで「ユーザーが指定したファイル名で保存したい」というケースを考えます。

string uploadRoot = @"C:\app\uploads";
string userFileName = @"..\..\report:2025/01/28?.csv";

string safeFileName = SafePath.SanitizeFileName(userFileName);
string unsafeRelativePath = safeFileName; // ここではサブフォルダなしとする

string safeFullPath = SafePath.BuildSafePath(uploadRoot, unsafeRelativePath);

Console.WriteLine(safeFullPath);
// 例: C:\app\uploads\____report_2025_01_28_.csv
C#

この流れで、

  • 不正文字(:/? など)が _ に置き換えられ
  • uploads 配下の絶対パスに正規化され
  • ルートからはみ出していないことが保証される

という「安全なパス」が手に入ります。


実務で意識したい細かいポイント

長すぎるパス・ファイル名

Windows には「パス長の制限」があります(古い環境だと 260 文字問題など)。
最近の .NET と OS 設定ではかなり緩和されていますが、「異常に長いパス」は依然としてトラブルの元です。

必要であれば、「一定以上の長さなら切り詰める」「ハッシュ化して短くする」といった処理も、サニタイズの一部として検討できます。

予約語(CON, PRN, AUX, NUL など)

Windows には、「ファイル名として使えない予約語」が存在します(CON, PRN, AUX, NUL など)。
Path.GetInvalidFileNameChars ではこれらは検出されないので、本当に厳密にやるなら別途チェックが必要です。

ただし、業務アプリでそこまで攻撃的な入力が来ない前提なら、
まずは「不正文字の除去+ルート配下チェック」だけでも十分な防御になります。


まとめ 「パス安全化」は“ファイル操作の前に必ず通すゲート”

パス安全化は、派手さはないけれど、業務システムを長く安定して動かすための「地味だけど超重要な基礎」です。

押さえておきたいポイントを整理すると、

  • OS が提供する Path.GetInvalidFileNameChars / GetInvalidPathChars で、不正文字を機械的に置き換える。
  • Path.GetFullPath で相対パスを正規化し、「最終的にどこを指しているか」をはっきりさせる。
  • 「このルートの配下だけ触る」という前提を置き、StartsWith 判定でルートからはみ出していないかをチェックする。
  • これらをユーティリティクラスにまとめて、ファイル操作の前に必ず通す“ゲート”として使う。

ここまでできると、「とりあえず文字列をそのまま File に渡す」世界から卒業して、
「入力がどんなに汚くても、まずは安全なパスに整えてから触る」という、プロっぽいコードに一歩近づきます。

タイトルとURLをコピーしました