惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Security Blog
Microsoft Security Blog
aimingoo的专栏
aimingoo的专栏
I
InfoQ
C
Check Point Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
Apple Machine Learning Research
Apple Machine Learning Research
Vercel News
Vercel News
Last Week in AI
Last Week in AI
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
Stack Overflow Blog
Stack Overflow Blog
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
B
Blog
T
The Blog of Author Tim Ferriss
H
Help Net Security
云风的 BLOG
云风的 BLOG

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日
mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日
2026-04-25 · via 暗无天日

Shell 脚本经常需要临时文件来存放中间数据,但很多人习惯手写一个固定路径(比如 /tmp/backup.log )就开始用了。这种写法在单用户、单进程的环境下不会出问题,一旦脚本被多人同时运行,或者系统上有恶意用户,就会触发竞态条件和符号链接攻击(symlink attack)。这篇文章要回答的核心问题是:为什么 mktemp 是创建临时文件的标准做法,以及在实际脚本中怎么用好它。

危险的手写临时文件

问题出在 /tmp 目录的共享性质上。所有用户、所有进程都能读写这个目录,所以你的脚本必须假设"别人也在 /tmp 里操作"。

竞态条件

假设你写了一个备份脚本,中间结果存到 /tmp/backup.log

echo "backup started at $(date)" > /tmp/backup.log
cat /tmp/backup.log

如果两个人同时跑这个脚本,后启动的那个会 覆盖 前一个的文件。更隐蔽的是,两个进程的 echocat 交替执行,你读到的可能是别人写的数据,而你以为的备份记录已经不见了。这就是竞态条件(race condition)——多个进程争抢同一个资源,执行顺序不确定,结果也不确定。

符号链接攻击(Symlink Attack)

竞态条件只是麻烦,符号链接攻击则是真正的安全威胁。攻击者可以预先在 /tmp 里放一个符号链接,指向敏感文件。你的脚本往"临时文件"写入时,实际写入的是攻击者指定的目标。

下面用一个安全的演示来展示这个攻击的原理。我们在一个受控目录里模拟整个过程:

bash
mkdir -p /tmp/mktemp-demo
echo "这是敏感数据,密码是 hunter2" > /tmp/mktemp-demo/sensitive.txt

ln -sf /tmp/mktemp-demo/sensitive.txt /tmp/mktemp-demo/fake-temp.log

echo "备份日志..." > /tmp/mktemp-demo/fake-temp.log

echo "=== 敏感文件现在的内容 ==="
cat /tmp/mktemp-demo/sensitive.txt
echo "=== 符号链接指向 ==="
ls -la /tmp/mktemp-demo/fake-temp.log

rm -rf /tmp/mktemp-demo
=== 敏感文件现在的内容 ===
备份日志...
=== 符号链接指向 ===
lrwxrwxrwx 1 lujun9972 users 30  4月24日 23:20 /tmp/mktemp-demo/fake-temp.log -> /tmp/mktemp-demo/sensitive.txt

关键在于 ln -sf 这一步:攻击者把 /tmp/mktemp-demo/fake-temp.log 变成了一个指向 sensitive.txt 的符号链接。脚本用 > 重定向写入时,Shell 会跟随符号链接,把数据写到 sensitive.txt 里。如果这个敏感文件是 /etc/passwd 或者 SSH 私钥,后果就很严重了。

mktemp 的工作原理

mktemp 是 GNU coreutils 的一部分,每个 Linux 发行版都预装了它。它用三个机制同时解决上面两个问题:

  1. 随机文件名 :在模板末尾的 X 字符位置填入随机字符(字母和数字),保证每次调用产生不同的文件名,彻底消除竞态条件
  2. 原子创建 :内部用 open() 系统调用加上 O_CREAT | O_EXCL 标志位,保证"检查文件是否存在"和"创建文件"是一个不可分割的操作——即使攻击者在 mktemp 生成名字和实际创建文件之间的微秒窗口里插入了符号链接, O_EXCL 也会让创建失败,不会跟随符号链接写入
  3. 安全权限 :自动设置权限为 600 (仅 owner 可读写),其他用户无法读取你的临时数据

这三个机制叠加在一起,让 mktemp 创建的临时文件同时具备了唯一性、原子性和私密性。

核心用法

1. 基本用法 + trap 清理

这是最值得记住的模式。用 mktemp 创建文件,把路径存到变量里,然后用 trap 注册退出时的清理动作:

bashTMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT
echo "临时文件路径: $TMPFILE"
echo "some data" > "$TMPFILE"
cat "$TMPFILE"

临时文件路径: /tmp/tmp.ZCNLZw66zR
some data

trap "rm -f $TMPFILE" EXIT 这行是关键:它告诉 Shell,不管脚本正常退出还是中途崩溃,都执行 rm -f 删除临时文件。没有这行的话, mktemp 创建的文件会一直留在 /tmp 里,日积月累就占满了磁盘。

2. 创建临时目录

脚本需要多个临时文件时(比如解压一个 tar 包),用 -d 创建目录:

bashWORKDIR=$(mktemp -d)
trap "rm -rf $WORKDIR" EXIT
echo "临时目录: $WORKDIR"
ls -ld "$WORKDIR"
echo "file1" > "$WORKDIR/a.txt"
echo "file2" > "$WORKDIR/b.txt"
ls -la "$WORKDIR"
临时目录: /tmp/tmp.66U76Ka8q3
drwx------ 2 lujun9972 users 40  4月24日 23:20 /tmp/tmp.66U76Ka8q3
总计 8
drwxrwx--  2 lujun9972 users   80  4月24日 23:20 .
drwxrwxrwt 17 root      root  1020  4月24日 23:20 ..
-rw-r--r-- 1 lujun9972 users    6  4月24日 23:20 a.txt
-rw-r--r-- 1 lujun9972 users    6  4月24日 23:20 b.txt

注意两点:目录权限是 drwx------ ,只有 owner 能进入和列出内容;清理时要用 rm -rf 而不是 rm -f ,否则删除目录会失败 ( rm 不加 -r 拒绝删除目录 ) 。

3. 自定义模板

调试时想在 /tmp 里一眼找到自己的临时文件,可以用自定义模板。模板的最后一个组成部分必须含有至少 3 个连续的 Xmktemp 会把最后一段连续的 X 替换成随机字符:

mktemp /tmp/myapp-XXXXXX
/tmp/myapp-ZlAGi2

不带路径的模板会在当前目录创建文件,这经常让人意外:

mktemp myapp-XXXXXX
myapp-HMl7WJ

要确保文件一定在 $TMPDIR/tmp 里,请用下面的 --tmpdir 参数。

4. 尊重用户的 TMPDIR 设置

$TMPDIR 是一个标准环境变量,很多程序(包括 mktemp )用它来决定临时文件放在哪里。如果不设置这个变量,默认值是 /tmp 。系统管理员可以通过设置 $TMPDIR 把临时文件重定向到更大的磁盘、或避开 noexec 挂载选项。

--tmpdir 参数让 mktemp 把文件放在 $TMPDIR 指定的目录(如果没设置则回退到 /tmp )。也可以用 -p /path 显式指定目录:

bashmktemp --tmpdir myapp-XXXXXX
export TMPDIR=/var/tmp
mktemp --tmpdir myapp-XXXXXX
/tmp/myapp-KdoJ82
/var/tmp/myapp-nWG3E6

为什么这很重要?因为有些系统管理员会把 /tmp 挂载为 tmpfs(内存文件系统),空间有限;或者把 /tmp 设为 noexec ,不能在里面执行脚本。用 --tmpdir 让脚本自动适应用户的环境配置,不需要硬编码路径。

注意: mktemp 还有一个 -t 参数也能实现类似效果,但它已被标记为废弃,新脚本建议使用 --tmpdir

5. 干跑模式:只生成名字不创建文件

--dry-run 返回一个唯一的文件名,但不在磁盘上创建任何东西。适合下一个命令自己会创建文件的场景(比如 tar 解压到新目录、 ssh-keygen 生成密钥):

mktemp --dry-run --tmpdir staging-XXXXXX
/tmp/staging-XcBg1g

注意: --dry-run 返回的名字 不保证 后续一定可用——在你使用这个名字之前,别的进程可能已经创建了同名文件。所以它只在"下一个命令会原子创建"的场景下安全。

进阶场景

在管道中使用

管道的一个经典限制是:读取端和写入端必须在管道启动时就接好,不能在管道中间插入一个"先处理再传递"的步骤。临时文件可以打破这个限制。

下面这个例子展示了一个常见的场景:把命令输出同时存到临时文件和继续传给下一个命令处理。

bashTMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT

ls -la /usr/bin | tee "$TMPFILE" | grep "bash"

echo "--- 临时文件中记录的总行数 ---"
wc -l < "$TMPFILE"
-rwxr-xr-x 1 root root 1162312 12月11日 06:02 bash
-r-xr-x 1 root root   7321 12月11日 06:02 bashbug
-rwxr-xr-x 1 root root  20411  4月 1日 05:17 env_parallel.bash
-rwxr-xr-x 1 root root    2756 2024年12月27日 globash
lrwxrwxrwx 1 root root       4 12月11日 06:02 rbash -> bash
lrwxrwxrwx 1 root root       4 12月11日 06:02 sh -> bash
--- 临时文件中记录的总行数 ---
4756

并行处理中的临时文件

xargsparallel 并行执行任务时,每个任务都需要独立的输出文件。 mktemp 可以在循环中为每个任务分配唯一的文件名:

bashRESULTDIR=$(mktemp -d)
trap "rm -rf $RESULTDIR" EXIT

for i in 1 2 3; do
  outfile="$RESULTDIR/result-$i.txt"
  echo "processing $i" > "$outfile" &
done
wait

echo "=== 所有结果 ==="
cat "$RESULTDIR"/result-*.txt
=== 所有结果 ===
processing 1
processing 2
processing 3

上面这个例子中, mktemp -d 创建了一个唯一的私有目录,所有临时文件都放在里面。循环变量 $i 保证了同一脚本内并行任务之间不会冲突,但两个用户同时跑脚本时, mktemp 的唯一目录能确保两批文件互不干扰,而且 drwx------ 的目录权限防止其他用户读取你的中间数据。

Makefile 中的临时文件

Make 的 recipe 是在子 Shell 中执行的,多个 recipe 可能同时运行 ( make -j ) 。临时文件命名冲突在并行构建中尤其常见:

%.processed: %.raw
    TMPFILE=$$(mktemp) && \
    trap "rm -f $$TMPFILE" EXIT && \
    sed 's/old/new/g' $< > $$TMPFILE && \
    mv $$TMPFILE $@

这里必须用 $$(mktemp) (两个 $$ 是因为 Makefile 中的 $ 需要转义) ,而不能用固定路径。 make -j4 并行构建时,四个 recipe 同时执行,共享一个固定路径必然冲突。

速查表

参数 作用 示例
(无参数) $TMPDIR/tmp 创建临时文件 mktemp
-d 创建临时目录 mktemp -d
--tmpdir $TMPDIR/tmp 创建带模板名称的文件 mktemp --tmpdir myapp-XXXXXX
-p 指定创建位置 mktemp -p /var/cache XXXXXX
--dry-run 只返回名字,不创建文件 mktemp --dry-run --tmpdir x-XXXXXX
常见错误 正确做法
mktemp 后没保存返回路径 TMPFILE=$(mktemp) 保存到变量
忘记 trap 清理 trap "rm -f $TMPFILE" EXIT 紧跟 mktemp
rm -f 删除 mktemp -d 目录 rm -rf 删除目录
模板少于 3 个 X 至少 3 个 X ,推荐 6 个
硬编码 /tmp/ 路径 --tmpdir 参数尊重 $TMPDIR 设置