多种语言---安全的文件操作示例
文件校验方式
读取或者写入文件时必须文件进行校验,防止软连接攻击或者提权攻击,如果校验后再打开文件操作,很容易被构造条件竞争攻击。因此较安全的方式是先将文件打开,然后再校验,校验不通过时关闭文件,打开文件后文件不可能再被修改。
常见文件相关攻击路径
OOM(Out of Memory):
角色: 所有
原理: 构造超大文件或者超长数据报文,程序一次性读入到内存中,导致内存不够导致被操作系统杀掉或者进入死循环。
防御:
- 读文件前校验文件类型,不能是/dev/zero、/dev/urandom等设备类型
- 读取前校验文件大小是否超过预期中的最大值
- 读取文件、收取数据时传入最大期望大小
root修改普通用户的文件权限
角色: root
原理: root用户修改普通用户文件的权限。普通用户将文件改成软连接指向root自身的某个文件。导致root实际修改了自身文件的权限。普通用户可将系统任意文件修改为特定权限。可对系统可用性造成影响。
防御:
- root不应修改普通用户文件的权限。
- root修改文件权限前必须校验不是软连接
- chown命令必须加-h选项
- root用chmod修改普通用户文件的权限时, 先切换到普通用户再做修改
- 明确文件owner,root修改时应校验文件owner为期望值
root写普通用户的文件,导致任意文件破坏
角色: root
原理: root写普通用户文件。普通用户将文件改成软连接指向系统任意文件。由于root可写任意文件,可导致任意文件被破坏。
防御:
- root写普通用户文件前必须校验文件是否为软连接。
- root写普通用户文件前必须校验文件owner是否是期望的owner
root写普通用户的目录中的某个文件,导致特定文件破坏
角色: root
原理: root到普通的目录写入某个文件。普通用户将目录软连到/etc,/usr等系统目录。可导致将特定文件放到任意位置,当特定文件与系统文件重名时,可破坏系统可用性
防御:
- root写普通用户文件前必须判断文件上层目录是否是软连接,需向上递归到root自己的目录为止
root执行普通用户的文件
角色: root
原理:
- root执行普通用户的脚本或者二进制
- root加载普通用户的动态库
- root解析普通用户的配置文件,并执行其中内容
防御:
- 禁止root执行普通用户的脚本或者二进制
- 禁止root加载普通用户的动态库
- 禁止root加载普通用户的配置文件,如果必须加载必须做充分校验
判断文件是否存在
打开文件前不需要判断文件存在,通过打开文件的成功和失败来判断文件是否存在。
- 错误的写法
1
|
if (access(file_path, F_OK) != 0) { |
2
|
log("file not exist") |
3
|
return -1; |
4
|
} |
5
|
// access 和实际打开存在时间差。 |
6
|
int fd = open(file_path); |
7
|
read(fd, buf, size); |
- 正确的写法
1
|
int fd = open(file_path); |
2
|
if (fd < 0) { |
3
|
log("open failed: %s", strerror(errno)); // 可通过errno记录失败原因 |
4
|
return -1; |
5
|
} |
6
|
read(fd, buf, size); |
C 文件校验
1
|
#include <sys/types.h> |
2
|
#include <sys/stat.h> |
3
|
#include <unistd.h> |
4
|
#include <fcntl.h> |
5
|
|
6
|
#define MAX_FILE_SIZE (1024 * 10) |
7
|
|
8
|
// 系统调用方式 |
9
|
struct stat st = {0}; |
10
|
// 打开是可选择是否打开软连接,当设置O_NOFOLLOW时,如果文件是软连接,将打开失败 |
11
|
int fd = open(file_path, O_NOFOLLOW | O_RDONLY); |
12
|
if (fd < 0) { |
13
|
return -1; |
14
|
} |
15
|
|
16
|
if (fstat(fd, &st) != 0 |
17
|
|| st.st_size > MAX_FILE_SIZE // 校验文件大小 |
18
|
|| st.st_uid != os.geteuid // 校验文件owner |
19
|
|| S_ISLNK(st.st_mode)) { // 校验是否是软连接 |
20
|
return -1; |
21
|
} |
22
|
close(fd) |
23
|
|
24
|
// 文件流方式 |
25
|
FILE *file = fopen(file_path, "rb"); |
26
|
if (file == NULL) { |
27
|
return -1; |
28
|
} |
29
|
// 打开后校验 1. 文件大小 2. 软连接 |
30
|
if (fstat(fileno(file), &st) !=0 || |
31
|
|| st.st_size > MAX_FILE_SIZE // 校验文件大小 |
32
|
|| st.st_uid != os.geteuid() // 校验文件owner |
33
|
|| S_ISLNK(st.st_mode)) { // 校验是否是软连接 |
34
|
return -1; |
35
|
} |
36
|
|
37
|
|
38
|
// 如果需要修改文件权限,也需要校验后通过fd修改 |
39
|
int fchmod(fd, S_IRUSR | S_IWUSR); |
40
|
fclose(file); |
C++ 文件校验
1
|
ifstream input(file_path, std::ios::binary | std::ios::ate); |
2
|
if (!input.is_open()) { |
3
|
return FAILED; |
4
|
} |
5
|
// 校验文件大小 |
6
|
if (input.tellg() > MAX_FILE_SIZE) { |
7
|
return FAILED; |
8
|
} |
9
|
// 校验完之后需要将偏移回文件开头 |
10
|
input.seekg(0, std::ios::beg); |
- 注意C++为了保持操作系统之间的兼容性,并未提供获取fd和FILE的标准方式,因此无法实现打开后通过文件描述符校验文件owner和修改文件权限等较安全的操作。文件操作应使用C方式。
Python 文件校验
1
|
import os |
2
|
import stat |
3
|
with open(file_path) as f: |
4
|
file_info = os.stat(f.fileno()) |
5
|
# 校验文件是否是软连接 |
6
|
if stat.S_ISLNK(file_info.st_mode): |
7
|
raise ... |
8
|
# 校验文件大小 |
9
|
if file_info.st_size > MAX_SIZE: |
10
|
raise ... |
11
|
# 校验文件owner |
12
|
if file_info.st_uid != os.geteuid(): |
13
|
raise ... |
14
|
# 如果需要修改文件权限 |
15
|
os.fchmod(f.fileno(), 0o600) |
- 注意: Python的read函数默认会读取文件所有内容,所以调用read一定要传入期望的最大文件大小,防止OOM
1
|
content = file.read(MAX_SIZE) |
go 文件校验
1
|
file, err := os.Open(file_path) |
2
|
if err != nil { |
3
|
return err |
4
|
} |
5
|
defer file.Close() |
6
|
file_info, err := file.Stat() |
7
|
if err != nil { |
8
|
return err |
9
|
} |
10
|
// 校验文件大小 |
11
|
if file_info.Size() > MAX_SIZE { |
12
|
return errors.New(fmt.Sprintf("file size error %v", file_info.Size())) |
13
|
} |
14
|
// 校验文件是否软连接 |
15
|
if (file_info.Mode() & fs.ModeSymlink) != 0 { |
16
|
return errors.New("file is softlink") |
17
|
} |
18
|
|
19
|
// 校验文件owner |
20
|
if st := file_info.Sys(); st.(*syscall.Stat_t).Uid != uint32(os.Geteuid()) { |
21
|
return errors.New("file owner incorrect") |
22
|
} |
23
|
|
24
|
// 如果需要修改文件权限 |
25
|
if err := file.Chmod(0600); err != nil { |
26
|
return err |
27
|
} |
- 注意 Go中的ioutil.ReadAll函数会一次性读入文件所有内容,不建议使用
本文来自博客园,作者:易先讯,转载请注明原文链接:https://www.cnblogs.com/gongxianjin/p/16911359.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通