TOCTOU竞态漏洞详解与防护

发布时间:2026/7/15 9:42:05
TOCTOU竞态漏洞详解与防护 TOCTOU竞态漏洞详解与防护特权程序写临时文件、先access再open、多线程里if (balance x)再扣款——检查Check与使用Use之间若可被他人插入修改就是TOCTOUTime-of-Check to Time-of-Use一类经典的竞态条件Race Condition与安全漏洞在文件系统、权限校验、共享状态里极常见。速览本质Check 与 Use不是原子操作攻击者在窗口内换掉资源程序按过期假设行动。文件系统重灾区access()open()、先判断存在再create、符号链接切换。首选防护原子完成「检查使用」——O_CREAT|O_EXCL、对fd操作而非路径。原则用操作失败代替预检查O_NOFOLLOW特权程序禁用access()做安全决策。内存竞态锁或CAS包裹整个 check-use 临界区。核心结论30 秒版不要 Check 再 Use——尽量Atomic 地 Do。临时文件open(..., O_CREAT|O_EXCL|O_WRONLY, 0600)。打开后fstat(fd)/fchmod(fd)别再信任路径名。setuid 程序永不access() 特权open()。多线程if检查与赋值必须同一互斥区或原子指令。决策树我在防什么Check 与 Use 是否分离 ├─ 是且中间可被他人/他线程改变资源 │ ├─ 文件路径 → O_EXCL / openfd / O_NOFOLLOW │ ├─ 共享变量 → mutex 或 CAS 包裹整段 │ └─ 业务状态 → 数据库事务 / 乐观锁版本号 └─ 否单次原子 syscall 或锁内完成→ 一般无 TOCTOU目录1. 什么是 TOCTOU2. 文件系统经典案例3. 常见场景4. 为何难发现5. 防护策略总览6. 文件系统安全写法6.6 防护边界难以 100% 杜绝7. 特权程序与多线程8. 各语言 API 速查9. 常见误区10. 延伸阅读1. 什么是 TOCTOUTOCTOUTime-of-Check检查时刻与Time-of-Use使用时刻之间的缝隙。程序先验证某条件文件可写、不存在、权限合法、余额足够在验证通过之后、真正使用该资源之前另一线程/进程/用户改变了资源导致操作对象与检查时不一致。攻击者文件系统程序攻击者文件系统程序实际写的是敏感文件Check如 access 可写OK将 path 换为指向 /etc/passwd 的 symlinkUseopen 并写入概念说明Check基于当前快照的判断存在性、权限、类型Use打开、读写、删除、扣款等实际副作用根因两次操作非原子中间状态可变别名Check-Then-Act、Double-Fetch安全语境2. 文件系统经典案例2.1access()open()UNIX symlink 攻击if(access(important.txt,W_OK)0){/* Check当前用户可写 */fdopen(important.txt,O_WRONLY);/* Use打开写入 *//* write(fd, ...) */}攻击窗口access成功后、open前攻击者把important.txt换成指向/etc/passwd的符号链接。若程序以elevated 权限运行open可能按链接目标写入系统文件。Check 通过的是原路径当时的权限Use 操作的是已被替换的对象2.2 存在性检查再创建if(!file_exists(path)){create_file(path);}在「不存在」与「创建」之间攻击者可创建同名文件或符号链接导致覆盖敏感文件、权限逃逸、写入错误目标2.3 临时目录预测攻击在/tmp等全局可写目录用可预测文件名/tmp/app.pid程序检查「文件不存在」攻击者抢先mkfifo/symlink程序创建或打开时踩坑历史上sudo、glibc tmpfile等 CVE 多与临时文件 TOCTOU 相关。3. 常见场景场景Check → Use 模式文件系统权限/存在/类型检查 →open/rename/unlinksetuid 程序用调用者身份access→ 用 root 身份open多线程if (x 0)→ 修改x临时文件不存在 →creat/open配置/锁校验版本或签名 → 加载内容业务逻辑查库存/余额 → 下单扣减4. 为何难发现原因说明低概率窗口极窄功能测试常碰不到需并发单线程、低负载难复现跨 syscall静态分析难追踪两次调用间的时序环境依赖本地难现生产/多用户目录才暴露窗口有多窄在 Linux 上Check 与 Use 之间往往只有纳秒微秒级的 CPU 时间看似「来不及插一脚」。但在多核、抢占式调度、页错误、上下文切换下另一线程/进程仍可能插入——本地单线程压测难复现生产多用户、高负载、共享/tmp时概率显著上升。这也解释了「为什么大家还在犯」功能测试几乎永远绿灯。模糊测试、并发压力测试、Code Review 关注Check-Then-Act模式比纯单元测试更有效。5. 防护策略总览策略要点原子化一次 syscall 完成 checkuseO_EXCL等用 fd 不用 pathopen后fstat/read不再解析路径失败代替预判直接open用errno处理禁跟 symlinkO_NOFOLLOW、受控工作目录锁/事务flock、DB 事务、mutex 包住 checkuse最小权限setuid 尽早降权不在用户可控目录操作口诀不要 Check 再 Use尽量 Atomic 地 Do用 fd 不用路径用失败代替预判禁 symlink降权限锁住临界区。6. 文件系统安全写法6.1 安全创建消除「存在性检查 创建」intfdopen(path,O_CREAT|O_EXCL|O_WRONLY,0600);if(fd0){if(errnoEEXIST){/* 已存在按业务处理 */}/* 其他错误 */}O_EXCL与O_CREAT同用仅当文件不存在时创建内核原子完成无 TOCTOU 窗口。临时文件更推荐/* POSIXmkstemp 模板由内核生成唯一名 */chartmpl[]/tmp/app-XXXXXX;intfdmkstemp(tmpl);6.2 打开后信任 fd而非路径intfdopen(path,O_RDONLY|O_NOFOLLOW);if(fd0){/* 处理错误 */}structstatst;if(fstat(fd,st)!0){close(fd);return-1;}/* 校验 st.st_uid、st.st_mode、是否为常规文件等 *//* 后续 read/write/fchmod 均针对 fd */避免access()open()打开后用fstat(fd)校验类型/属主/权限后续用fchmod(fd)/fchown(fd)操作已打开对象勿再解析可能被换链的路径。6.3 禁止符号链接跟随与openat工作目录fdopen(path,O_RDONLY|O_NOFOLLOW);在攻击者可控目录如/tmp、用户 home 下随机路径操作时使用O_NOFOLLOW避免 TOCTOU 间路径被换链容器 / setuid / sandbox 场景推荐先打开受控目录为dirfd再相对打开其内文件减少路径穿越与目录级竞态intdfdopen(/var/run/myapp,O_PATH|O_DIRECTORY|O_CLOEXEC);if(dfd0){/* 错误 */}intfdopenat(dfd,conf,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);/* 校验 fd 后读写dfd 指向的目录权限须仅 root/服务可写 */比「裸路径 O_NOFOLLOW」更稳工作目录本身不可被攻击者替换权限0700、非 world-writable。6.4 文件锁的局限flock/fcntl锁可序列化同一文件上的 checkuse但不能防止「Check 的是路径 A、Use 前 A 被换成指向 B 的 symlink」——锁的是 inode路径级 TOCTOU 仍要靠O_EXCL/O_NOFOLLOW/fd 语义。6.5 避免access()做安全决策/* 不推荐 */if(access(path,R_OK)0)fdopen(path,O_RDONLY);/* 推荐 */fdopen(path,O_RDONLY);if(fd0){/* EACCES、ENOENT 等 */}access()使用真实 UIDreal uid判断权限而 setuid 程序里open()往往按有效 UIDeffective uid执行——二者不一致时会出现「access 说不可写、open 却以 root 写成功」或相反的逻辑漏洞。6.6 防护边界难以 100% 杜绝即使使用O_EXCL、O_NOFOLLOW、openat在以下情况仍可能有TOCTOU 变种条件风险路径位于攻击者可控文件系统NFS、FUSE、恶意挂载等上的「原子创建」语义可能与本地 ext4 不同目录级竞态rename/RENAME_EXCHANGE等在 Check 与 Use 间换掉目录项工作目录可被写world-writable 的/tmp上操作可预测文件名内核保证的是同一本地、可信文件系统内单次 syscall 的原子性真正可靠的前提是操作目录攻击者无法写入且尽量在受控dirfd内用相对路径打开。安全编码能大幅降险但不能替代「别把特权程序放进用户目录」的部署原则。7. 特权程序与多线程7.1 setuid / 高权限程序原则说明不用access()决策检查与 open 的 uid 语义可能不同尽早 drop privilege能降权再操作不可信路径受控目录如/var/run/myapp-uid/权限严格直接 try errno失败即拒绝不预检7.2 内存中的 TOCTOU与数据竞争/* 错误check 与 use 分离 */if(balancewithdraw){balance-withdraw;}严格说这是 C/C 内存模型下的数据竞争Data Race从「检查–使用非原子」抽象看它同样是Check-Then-Act / TOCTOU在内存中的表现。修复思路一致别让 if 和修改之间留缝。另一线程可在if与-之间也扣款导致超扣或负余额。修复/* 互斥锁包裹整段 checkuse */pthread_mutex_lock(mu);if(balancewithdraw)balance-withdraw;pthread_mutex_unlock(mu);或使用原子 CAS无锁账户、无锁队列/* 伪代码仅当 balance w 时原子减 w */atomic_compare_exchange_weak(balance,expected,expected-withdraw);关键检查与修改必须在同一原子边界内锁、CAS、DBUPDATE ... WHERE balance ?。7.3 数据库与业务层SQL 事务、SELECT ... FOR UPDATE、乐观锁版本号本质是消除业务 TOCTOUUPDATEaccountsSETbalancebalance-100WHEREid1ANDbalance100;affected_rows 0表示 check 失败无需先SELECT再UPDATE两段式竞态。8. 各语言 API 速查语言安全临时文件 / 独占创建Cmkstemp、open(..., O_CREAT|O_EXCL, 0600)Cstd::filesystem仍要注意优先mkstemp封装或库Pythontempfile.NamedTemporaryFile、tempfile.mkstempJavaFiles.createFile(path, CREATE_NEW)、CREATE_NEWNOFOLLOW_LINKSGoos.OpenFile(name, os.O_CREATE|os.O_EXCL, 0600)os.CreateTempRusttempfile::NamedTempFile、OpenOptions::new().create_new(true)9. 常见误区误区事实「检查过就安全」检查的是过去快照Use 时对象可能已变access通过就能写setuid symlink → 经典 CVE 模式文件锁万能防不住路径被换链的跨对象 TOCTOU只有安全软件才用任何check-then-act业务都有单线程无 TOCTOU仍有信号处理程序、异步回调重入窗口/tmp随便命名用不可预测名O_EXCL/mkstemp10. 延伸阅读资源说明CWE-367Time-of-check Time-of-use Race ConditionCWE-362Concurrent Execution using Shared Resource (Race)CVE-2021-3156sudo 堆溢出常与特权程序安全一并讨论特权边界案例Qualys 等对sudo / glibc 临时文件历史分析临时目录 TOCTOU 类漏洞复盘man 2 open/man 2 openatO_EXCL、O_NOFOLLOW、O_PATHCERT C FIO45-C避免 TOCTOU 竞态的文件操作Code Review 时搜索模式access(、stat(open(、if.*existscreate、if ( 无锁写共享变量——对每一处追问Check 与 Use 之间谁还能改收束技术TOCTOU 的本质是把非原子的两步当成一步的信任文件系统用O_EXCL 与 fd并发用锁或 CAS业务用事务与条件更新——让「检查」和「使用」在同一条不可打断的语义里完成。一句话记法人话别问「能不能做」直接去做然后处理失败——与「用失败代替预判」同义更适合口头传播。验证环境POSIX.1 · Linux · glibc · C11 线程 · Java NIO.2