c# TOCTOU(Time-of-check to time-of-use)并发安全漏洞和防范

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 捕获 FileNotFoundExceptionUnauthorizedAccessException,并按需处理
  • 写入文件时,优先使用 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 块内,减少中间干扰点
  • 对关键路径加锁(lockSemaphoreSlim),仅适用于单进程内线程竞争;跨进程无效
  • 使用操作系统级原子操作:如 Windows 上通过 CreateFileCREATE_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)管理资源状态,把“是否存在”的判断从文件系统移到有事务/版本控制的存储中

真正棘手的是那些看似无害的“先看再做”逻辑,比如配置热重载监听文件变更后立刻重新加载——如果加载过程中文件被恶意覆盖,就可能执行未校验的代码。这类问题不会在单元测试里暴露,只在压测或生产突发流量时浮现。