TOCTOU竞态漏洞详解与防护
2026/7/15 9:41:54 网站建设 项目流程

TOCTOU竞态漏洞详解与防护

特权程序写临时文件、先accessopen、多线程里if (balance >= x)再扣款——检查(Check)使用(Use)之间若可被他人插入修改,就是TOCTOU(Time-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 秒版)

  1. 不要 Check 再 Use——尽量Atomic 地 Do
  2. 临时文件:open(..., O_CREAT|O_EXCL|O_WRONLY, 0600)
  3. 打开后fstat(fd)/fchmod(fd),别再信任路径名。
  4. setuid 程序:永不access()+ 特权open()
  5. 多线程:if检查与赋值必须同一互斥区原子指令

决策树(我在防什么?)

Check 与 Use 是否分离? ├─ 是,且中间可被他人/他线程改变资源 │ ├─ 文件路径 → O_EXCL / open+fd / O_NOFOLLOW │ ├─ 共享变量 → mutex 或 CAS 包裹整段 │ └─ 业务状态 → 数据库事务 / 乐观锁版本号 └─ 否(单次原子 syscall 或锁内完成)→ 一般无 TOCTOU

目录

  • 1. 什么是 TOCTOU
  • 2. 文件系统经典案例
  • 3. 常见场景
  • 4. 为何难发现
  • 5. 防护策略总览
  • 6. 文件系统安全写法
    • 6.6 防护边界:难以 100% 杜绝
  • 7. 特权程序与多线程
  • 8. 各语言 API 速查
  • 9. 常见误区
  • 10. 延伸阅读

1. 什么是 TOCTOU

TOCTOU=Time-of-Check(检查时刻)与Time-of-Use(使用时刻)之间的缝隙。

程序先验证某条件(文件可写、不存在、权限合法、余额足够),在验证通过之后、真正使用该资源之前,另一线程/进程/用户改变了资源,导致操作对象与检查时不一致。

攻击者文件系统程序攻击者文件系统程序实际写的是敏感文件Check(如 access 可写)OK将 path 换为指向 /etc/passwd 的 symlinkUse(open 并写入)
概念说明
Check基于当前快照的判断(存在性、权限、类型)
Use打开、读写、删除、扣款等实际副作用
根因两次操作非原子;中间状态可变
别名Check-Then-Act、Double-Fetch(安全语境)

2. 文件系统经典案例

2.1access()+open()(UNIX symlink 攻击)

if(access("important.txt",W_OK)==0){/* Check:当前用户可写? */fd=open("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):

  1. 程序检查「文件不存在」
  2. 攻击者抢先mkfifo/symlink
  3. 程序创建或打开时踩坑

历史上sudoglibc tmpfile等 CVE 多与临时文件 TOCTOU 相关。


3. 常见场景

场景Check → Use 模式
文件系统权限/存在/类型检查 →open/rename/unlink
setuid 程序用调用者身份access→ 用 root 身份open
多线程if (x > 0)→ 修改x
临时文件不存在 →creat/open
配置/锁校验版本或签名 → 加载内容
业务逻辑查库存/余额 → 下单扣减

4. 为何难发现

原因说明
低概率窗口极窄,功能测试常碰不到
需并发单线程、低负载难复现
跨 syscall静态分析难追踪两次调用间的时序
环境依赖本地难现,生产/多用户目录才暴露

窗口有多窄?在 Linux 上,Check 与 Use 之间往往只有纳秒~微秒级的 CPU 时间;看似「来不及插一脚」。但在多核、抢占式调度、页错误、上下文切换下,另一线程/进程仍可能插入——本地单线程压测难复现,生产多用户、高负载、共享/tmp时概率显著上升。这也解释了「为什么大家还在犯」:功能测试几乎永远绿灯。

模糊测试、并发压力测试、Code Review 关注Check-Then-Act模式,比纯单元测试更有效。


5. 防护策略总览

策略要点
原子化一次 syscall 完成 check+use(O_EXCL等)
用 fd 不用 pathopenfstat/read,不再解析路径
失败代替预判直接open,用errno处理
禁跟 symlinkO_NOFOLLOW、受控工作目录
锁/事务flock、DB 事务、mutex 包住 check+use
最小权限setuid 尽早降权;不在用户可控目录操作

口诀:不要 Check 再 Use,尽量 Atomic 地 Do;用 fd 不用路径,用失败代替预判;禁 symlink,降权限,锁住临界区。


6. 文件系统安全写法

6.1 安全创建(消除「存在性检查 + 创建」)

intfd=open(path,O_CREAT|O_EXCL|O_WRONLY,0600);if(fd<0){if(errno==EEXIST){/* 已存在,按业务处理 */}/* 其他错误 */}

O_EXCLO_CREAT同用:仅当文件不存在时创建,内核原子完成,无 TOCTOU 窗口。

临时文件更推荐:

/* POSIX:mkstemp 模板由内核生成唯一名 */chartmpl[]="/tmp/app-XXXXXX";intfd=mkstemp(tmpl);

6.2 打开后信任 fd,而非路径

intfd=open(path,O_RDONLY|O_NOFOLLOW);if(fd<0){/* 处理错误 */}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工作目录

fd=open(path,O_RDONLY|O_NOFOLLOW);

攻击者可控目录(如/tmp、用户 home 下随机路径)操作时:

  • 使用O_NOFOLLOW
  • 避免 TOCTOU 间路径被换链

容器 / setuid / sandbox 场景推荐:先打开受控目录dirfd,再相对打开其内文件,减少路径穿越与目录级竞态:

intdfd=open("/var/run/myapp",O_PATH|O_DIRECTORY|O_CLOEXEC);if(dfd<0){/* 错误 */}intfd=openat(dfd,"conf",O_RDONLY|O_NOFOLLOW|O_CLOEXEC);/* 校验 fd 后读写;dfd 指向的目录权限须仅 root/服务可写 */

比「裸路径 +O_NOFOLLOW」更稳:工作目录本身不可被攻击者替换(权限0700、非 world-writable)。

6.4 文件锁的局限

flock/fcntl锁可序列化同一文件上的 check+use,但不能防止「Check 的是路径 A、Use 前 A 被换成指向 B 的 symlink」——锁的是 inode,路径级 TOCTOU 仍要靠O_EXCL/O_NOFOLLOW/fd 语义。

6.5 避免access()做安全决策

/* 不推荐 */if(access(path,R_OK)==0)fd=open(path,O_RDONLY);/* 推荐 */fd=open(path,O_RDONLY);if(fd<0){/* EACCES、ENOENT 等 */}

access()使用真实 UID(real uid)判断权限,而 setuid 程序里open()往往按有效 UID(effective uid)执行——二者不一致时,会出现「access 说不可写、open 却以 root 写成功」或相反的逻辑漏洞。

6.6 防护边界:难以 100% 杜绝

即使使用O_EXCLO_NOFOLLOWopenat,在以下情况仍可能有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(balance>=withdraw){balance-=withdraw;}

严格说,这是 C/C++ 内存模型下的数据竞争(Data Race);从「检查–使用非原子」抽象看,它同样是Check-Then-Act / TOCTOU在内存中的表现。修复思路一致:别让 if 和修改之间留缝。

另一线程可在if-=之间也扣款,导致超扣或负余额。

修复

/* 互斥锁包裹整段 check+use */pthread_mutex_lock(&mu);if(balance>=withdraw)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、乐观锁版本号本质是消除业务 TOCTOU:

UPDATEaccountsSETbalance=balance-100WHEREid=1ANDbalance>=100;

affected_rows == 0表示 check 失败,无需先SELECTUPDATE两段式竞态。


8. 各语言 API 速查

语言安全临时文件 / 独占创建
Cmkstempopen(..., O_CREAT|O_EXCL, 0600)
C++std::filesystem仍要注意;优先mkstemp封装或库
Pythontempfile.NamedTemporaryFiletempfile.mkstemp
JavaFiles.createFile(path, CREATE_NEW)CREATE_NEW+NOFOLLOW_LINKS
Goos.OpenFile(name, os.O_CREATE|os.O_EXCL, 0600)os.CreateTemp
Rusttempfile::NamedTempFileOpenOptions::new().create_new(true)

9. 常见误区

误区事实
「检查过就安全」检查的是过去快照,Use 时对象可能已变
access通过就能写setuid + symlink → 经典 CVE 模式
文件锁万能防不住路径被换链的跨对象 TOCTOU
只有安全软件才用任何check-then-act业务都有
单线程无 TOCTOU仍有信号处理程序异步回调重入窗口
/tmp随便命名不可预测名+O_EXCL/mkstemp

10. 延伸阅读

资源说明
CWE-367Time-of-check Time-of-use Race Condition
CWE-362Concurrent Execution using Shared Resource (Race)
CVE-2021-3156(sudo 堆溢出;常与特权程序安全一并讨论)特权边界案例
Qualys 等对sudo / glibc 临时文件历史分析临时目录 TOCTOU 类漏洞复盘
man 2 open/man 2 openatO_EXCLO_NOFOLLOWO_PATH
CERT C FIO45-C避免 TOCTOU 竞态的文件操作

Code Review 时搜索模式:access(stat(+open(if.*exists+createif (+ 无锁写共享变量——对每一处追问:Check 与 Use 之间谁还能改?


收束(技术):TOCTOU 的本质是把非原子的两步当成一步的信任;文件系统用O_EXCL 与 fd,并发用锁或 CAS,业务用事务与条件更新——让「检查」和「使用」在同一条不可打断的语义里完成。

一句话记法(人话)别问「能不能做」,直接去做,然后处理失败——与「用失败代替预判」同义,更适合口头传播。

验证环境:POSIX.1 · Linux · glibc · C11 线程 · Java NIO.2

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询