はじめに 「パス安全化」は“ファイルを触る前の身だしなみ”
業務でファイルを扱うコードを書くとき、本当によく出てくるのが「パス文字列」です。
ユーザー入力、設定ファイル、外部システムから渡される値――それらをそのまま File.Open や Directory.CreateDirectory に渡すと、例外になったり、最悪「触ってはいけない場所」にアクセスしてしまうことがあります。
そこで必要になるのが「パス安全化(サニタイズ)」です。
ざっくり言うと、
- OS 的に不正な文字を取り除く(または置き換える)
- 相対パスを正規化して、「どこを指しているか」をはっきりさせる
- 想定外の場所(上位ディレクトリなど)に出ていかないようにチェックする
といったことを、ファイル操作の前にきちんとやる、という考え方です。
ここでは、C# 初心者向けに、「パス安全化」の基本と、実務でそのまま使えるユーティリティ例を、かみ砕いて説明していきます。
不正な文字を取り除く・置き換える
なぜ「そのままの文字列」では危ないのか
Windows には、「ファイル名やパスに使ってはいけない文字」が決まっています。
例えば * や ?、:、"、<、>、| などです。
こういった文字が含まれていると、File.Create や Directory.CreateDirectory で NotSupportedException が飛びます。
ユーザー入力や外部データからパスを組み立てるときは、まず「その文字列が 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.Combine → Path.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 に渡す」世界から卒業して、
「入力がどんなに汚くても、まずは安全なパスに整えてから触る」という、プロっぽいコードに一歩近づきます。
