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

来源:这里教程网 时间:2026-02-21 17:37:03 作者:

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)管理资源状态,把“是否存在”的判断从文件系统移到有事务/版本控制的存储中

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

相关推荐