TOCTOU是C#中因检查与使用间存在时间窗口导致的逻辑漏洞,表现为File.Exists后文件被删、Directory.Exists后目录已存在等;应改用原子操作如Directory.CreateDirectory、File.ReadAllText配合异常处理,跨进程需用原子重命名或分布式协调服务。

TOCTOU 在 C# 中的真实表现形式
TOCTOU 不是 .NET 运行时抛出的异常,而是一类逻辑漏洞:代码先检查某个条件(如 File.Exists(path)),再基于该结果执行操作(如 File.ReadAllText(path)),但两次调用之间文件可能被删除、替换或权限变更。这种竞态在多线程、多进程甚至跨服务场景下都会触发。
常见错误模式包括:
- 先用
Directory.Exists()判断目录存在,再调用Directory.CreateDirectory()—— 可能抛出IOException:“目录已存在”或“拒绝访问” - 先用
File.Exists()检查文件,再用new FileStream(path, FileMode.Open)打开 —— 可能抛出FileNotFoundException - 检查 ACL 或文件属性后决定是否读取,但检查后文件被篡改
用原子操作替代检查+使用组合
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");
}需要显式检查时,如何降低风险
某些场景无法避免检查(例如日志中记录“跳过不存在的配置文件”),此时应尽量缩短检查到使用的间隔,并配合其他防护手段:
- 将检查和使用放在同一 try 块内,减少中间干扰点
- 对关键路径加锁(
lock或SemaphoreSlim),仅适用于单进程内线程竞争;跨进程无效 - 使用操作系统级原子操作:如 Windows 上通过
CreateFile带CREATE_ALWAYS标志打开文件,比“检查+创建”更可靠 - 避免在高敏感逻辑中依赖
File.GetAttributes()等易被绕过的元数据检查
跨进程 TOCTOU 更难防御,必须换设计思路
当多个进程(如 Web API + 后台任务)共享同一文件或目录时,.NET 层面的锁完全失效。此时 File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None) 会失败,但 FileShare.Read 又无法阻止其他进程删除文件。
可行方案只有两类:
- 用临时重命名 + 原子提交:写入到
file.tmp,再用File.Move("file.tmp", "file.dat")—— Windows/Linux 下该操作是原子的(同卷内) - 改用数据库或专用协调服务(如 Redis 分布式锁、ZooKeeper)管理资源状态,把“是否存在”的判断从文件系统移到有事务/版本控制的存储中
真正棘手的是那些看似无害的“先看再做”逻辑,比如配置热重载监听文件变更后立刻重新加载——如果加载过程中文件被恶意覆盖,就可能执行未校验的代码。这类问题不会在单元测试里暴露,只在压测或生产突发流量时浮现。










