文件操作安全:文件路径遍历、符号链接攻击、临时文件安全、文件权限管理

文件操作,说白了就是程序跟磁盘打交道的那点事。很多开发者觉得读写文件嘛,简单得很。但我见过太多系统,就是因为文件操作没处理好,被人从底层捅了个大窟窿。今天咱们就把这块掰开揉碎了讲清楚。

一、文件路径遍历攻击

这是最常见的一类漏洞。攻击者通过构造特殊的路径字符串,绕过你的限制,读到不该读的文件。

核心问题:用户可控的路径没有做合法性校验。

举个例子,你的程序允许用户下载日志文件:

// 有问题的代码
char path[256];
snprintf(path, sizeof(path), "/var/log/app/%s", user_input);
FILE *fp = fopen(path, "r");

用户传个 ../../etc/passwd,你猜会发生什么?

我在项目中遇到过类似的情况。当时一个内部工具,允许用户指定输出文件路径。测试人员传了个 ../../../etc/shadow,直接把系统密码文件给覆盖了。嗯,那天的复盘会开得特别长。

防御方案

  1. 路径规范化:用 realpath() 把路径转成绝对路径
  2. 前缀检查:确认规范化后的路径以允许的目录开头
  3. 拒绝特殊字符:过滤 ../
// 安全的做法
char *real = realpath(user_input, NULL);
if (real == NULL) {
    // 路径不存在或出错
    return -1;
}
// 检查是否在允许的目录下
if (strncmp(real, "/var/log/app/", 13) != 0) {
    free(real);
    return -1;
}

我的习惯:永远先调用 realpath(),再做白名单检查。别用黑名单,你永远想不到攻击者会用什么姿势绕过。

二、符号链接攻击

符号链接(symlink)是 Linux 里一个很实用的功能,但也经常被用来搞破坏。攻击者会在你程序要创建文件的位置,提前放一个符号链接,指向系统关键文件。

你想想看,你的程序以 root 权限运行,准备在 /tmp/xxx 写个临时文件。结果这个路径已经被攻击者用符号链接指向了 /etc/passwd。你写的内容,就直接覆盖了系统文件。

典型场景

攻击方式 说明
TOCTOU 竞争 检查文件是否存在和创建文件之间,攻击者插入符号链接
预创建陷阱 攻击者提前在目标路径创建符号链接
目录遍历+符号链接 组合攻击,先遍历到可写目录,再创建符号链接

我曾经踩过的坑:写一个日志轮转工具,用了 access() 检查文件是否存在,然后 open() 创建。结果线上环境被人用符号链接把日志文件指向了 /dev/sda,直接把磁盘写满了。从那以后,我再也不用 access() + open() 这种组合了。

防御方案

// 使用 O_NOFOLLOW 和 O_CREAT 的组合
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
if (fd == -1) {
    // 文件已存在或路径是符号链接
    perror("open");
    return -1;
}

O_NOFOLLOW 这个标志很关键。它告诉内核:如果路径最后一个组件是符号链接,直接返回错误,别跟着跳。说白了就是「我不信任你给我的路径」。

三、临时文件安全

临时文件这块,我见过太多想当然的写法了。很多人觉得 /tmp 目录嘛,临时用用,无所谓。其实这里水很深。

常见问题

  • 可预测的文件名:用 pid 或时间戳命名,攻击者可以提前创建
  • 不清理临时文件:程序异常退出时,临时文件残留
  • 权限设置不当:临时文件可被其他用户读取

正确的做法

// 使用 mkstemp 创建安全的临时文件
char template[] = "/tmp/myapp_XXXXXX";
int fd = mkstemp(template);
if (fd == -1) {
    perror("mkstemp");
    return -1;
}
// 立即取消链接,但保持文件描述符可用
unlink(template);
// 现在可以安全地读写 fd 了
write(fd, data, len);
close(fd);

我个人习惯:创建临时文件后立刻 unlink()。这样即使程序崩溃,文件也会被内核自动回收。你想想看,多省心。

另外,tmpfile() 函数也是个好选择。它自动创建匿名临时文件,关闭时自动删除。不过要注意,有些系统上 tmpfile() 默认权限可能太宽松。

四、文件权限管理

文件权限这块,说白了就是「谁可以干什么」。很多 C 程序员只关注功能实现,权限管理随便设个 0777 就完事了。这种习惯很危险。

最小权限原则

创建文件时,只给必要的权限。别图省事用 0777。

// 创建配置文件,只有所有者可读写
int fd = open("config.ini", O_WRONLY | O_CREAT, 0600);

// 创建可执行文件,所有者可读写执行,组和其他人只读执行
int fd = open("script.sh", O_WRONLY | O_CREAT, 0755);

umask 的影响

很多新手不知道,进程的 umask 会影响 open()creat() 创建的权限。你明明传了 0666,结果 umask 是 0022,实际权限变成了 0644。

// 临时修改 umask,确保权限可控
mode_t old_umask = umask(0);
int fd = open("secret.txt", O_WRONLY | O_CREAT, 0600);
umask(old_umask);  // 恢复原来的 umask

注意:umask() 是线程不安全的。多线程程序里要小心使用。我一般建议在程序启动时就设置好全局 umask,后面就不动了。

目录权限的特殊性

目录的权限和文件不太一样。目录的「执行权限」意味着能否进入该目录。很多人只关注文件的权限,忽略了目录的权限设置。

目录权限 含义
r (读) 可以列出目录内容
w (写) 可以在目录中创建/删除文件
x (执行) 可以进入目录(cd 进去)

举个例子,如果你想让用户能进入目录但不能列出内容,可以设置 --x 权限。用户知道文件名就能访问,但不知道目录里有什么文件。

知识体系总览

下面这张图,是我梳理的文件操作安全的核心逻辑。你可以把它当作一个检查清单。

文件操作安全知识体系 路径遍历攻击 • 使用 realpath() • 白名单前缀检查 • 拒绝 .. 和 / • 规范化后再校验 符号链接攻击 • O_NOFOLLOW 标志 • O_EXCL 排他创建 • 避免 TOCTOU 竞争 • 检查文件真实路径 临时文件安全 • mkstemp() 创建 • 创建后立即 unlink • 使用 tmpfile() • 避免可预测文件名 文件权限管理 • 最小权限原则 • 控制 umask • 目录权限特殊处理 • 避免 0777 权限 核心原则:永远不要信任用户输入,永远检查真实路径,永远设置最小权限 防御要层层叠加,单一防护措施容易被绕过 实践建议 1. 代码审查时重点检查文件操作相关代码 2. 使用静态分析工具扫描路径遍历漏洞 3. 定期审计文件权限设置 4. 临时文件目录使用 tmpfs

文件操作安全,说白了就是「信任边界」的问题。你永远不知道用户会传什么进来,也不知道系统里有什么陷阱等着你。保持警惕,多用标准库提供的安全函数,少自己造轮子。嗯,做到这些,大部分坑你都能躲过去。


公众号:蓝海资料掘金营,微信deep3321