文件校验方式
读取或者写入文件时必须文件进行校验,防止软连接攻击或者提权攻击,如果校验后再打开文件操作,很容易被构造条件竞争攻击。因此较安全的方式是先将文件打开,然后再校验,校验不通过时关闭文件,打开文件后文件不可能再被修改。
常见文件相关攻击路径
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函数会一次性读入文件所有内容,不建议使用