文件操作安全:文件路径遍历、符号链接攻击、临时文件安全、文件权限管理
文件操作,说白了就是程序跟磁盘打交道的那点事。很多开发者觉得读写文件嘛,简单得很。但我见过太多系统,就是因为文件操作没处理好,被人从底层捅了个大窟窿。今天咱们就把这块掰开揉碎了讲清楚。
一、文件路径遍历攻击
这是最常见的一类漏洞。攻击者通过构造特殊的路径字符串,绕过你的限制,读到不该读的文件。
核心问题:用户可控的路径没有做合法性校验。
举个例子,你的程序允许用户下载日志文件:
// 有问题的代码
char path[256];
snprintf(path, sizeof(path), "/var/log/app/%s", user_input);
FILE *fp = fopen(path, "r");
用户传个 ../../etc/passwd,你猜会发生什么?
我在项目中遇到过类似的情况。当时一个内部工具,允许用户指定输出文件路径。测试人员传了个 ../../../etc/shadow,直接把系统密码文件给覆盖了。嗯,那天的复盘会开得特别长。
防御方案
- 路径规范化:用
realpath()把路径转成绝对路径 - 前缀检查:确认规范化后的路径以允许的目录开头
- 拒绝特殊字符:过滤
..、/等
// 安全的做法
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 权限。用户知道文件名就能访问,但不知道目录里有什么文件。
知识体系总览
下面这张图,是我梳理的文件操作安全的核心逻辑。你可以把它当作一个检查清单。
文件操作安全,说白了就是「信任边界」的问题。你永远不知道用户会传什么进来,也不知道系统里有什么陷阱等着你。保持警惕,多用标准库提供的安全函数,少自己造轮子。嗯,做到这些,大部分坑你都能躲过去。
公众号:蓝海资料掘金营,微信deep3321