TOCTOU是C#中因检查与使用间存在时间窗口导致的逻辑漏洞,表现为File.Exists后文件被删、Directory.Exists后目录已存在等;应改用原子操作如Directory.CreateDirectory、File.ReadAllText配合异常处理,跨进程需用原子重命名或分布式协调服务。
TOCTOU 不是 .NET 运行时抛出的异常,而是一类逻辑漏洞:代码先检查某个条件(如 File.Exists(path)),再基于该结果执行操作(如 File.ReadAllText(path)),但两次调用之间文件可能被删除、替换或权限变更。这种竞态在多线程、多进程甚至跨服务场景下都会触发。
常见错误模式包括:
Directory.Exists() 判断目录存在,再调用 Directory.CreateDirectory() —— 可能抛出 IOException:“目录已存在”或“拒绝访问”File.Exists() 检查文件,再用 new FileStream(path, FileMode.Open) 打开 —— 可能抛出 FileNotFoundException
C# 的 IO 类型多数提供“尝试即用”式方法,绕过显式检查环节,直接在单次系统调用中完成判断与操作,从根本上消除时间窗口。
推荐做法:
Directory.CreateDirectory(path) —— 它本身是幂等的,即使目录已存在也不报错,返回现有 DirectoryInfo
File.Exists(),而是用 try/catch 捕获 FileNotFoundException 和 UnauthorizedAccessException,并按需处理File.WriteAllText(path, content) 或 File.AppendAllText(path, content) —— 它们内部不依赖前置检查,失败即抛异常try
{
string content = File.ReadAllText(@"C:\temp\data.txt");
Process(content);
}
catch (FileNotFoundException)
{
// 文件在检查后被删了?现在直接处理缺失情况
Log.Warn("Expected file missing at read time");
}
catch (UnauthorizedAccessException)
{
// 权限在检查后被收回
Log.Error("Access denied during read");
}某些场景无法避免检查(例如日志中记录“跳过不存在的配置文件”),此时应尽量缩短检查到使用的间隔,并配合其他防护手段:
lock 或 SemaphoreSlim),仅适用于单进程内线程竞争;跨进程无效CreateFile 带 CREATE_ALWAYS 标志打开文件,比“检查+创建”更可靠File.GetAttributes() 等易被绕过的元数据检查当多个进程(如 Web API + 后台任务)共享同一文件
或目录时,.NET 层面的锁完全失效。此时 File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None) 会失败,但 FileShare.Read 又无法阻止其他进程删除文件。
可行方案只有两类:
file.tmp,再用 File.Move("file.tmp", "file.dat") —— Windows/Linux 下该操作是原子的(同卷内)真正棘手的是那些看似无害的“先看再做”逻辑,比如配置热重载监听文件变更后立刻重新加载——如果加载过程中文件被恶意覆盖,就可能执行未校验的代码。这类问题不会在单元测试里暴露,只在压测或生产突发流量时浮现。