多种语言---安全的文件操作示例

文件校验方式

读取或者写入文件时必须文件进行校验,防止软连接攻击或者提权攻击,如果校验后再打开文件操作,很容易被构造条件竞争攻击。因此较安全的方式是先将文件打开,然后再校验,校验不通过时关闭文件,打开文件后文件不可能再被修改。

常见文件相关攻击路径

OOM(Out of Memory):

角色: 所有
原理: 构造超大文件或者超长数据报文,程序一次性读入到内存中,导致内存不够导致被操作系统杀掉或者进入死循环。
防御:

  1. 读文件前校验文件类型,不能是/dev/zero、/dev/urandom等设备类型
  2. 读取前校验文件大小是否超过预期中的最大值
  3. 读取文件、收取数据时传入最大期望大小

root修改普通用户的文件权限

角色: root
原理: root用户修改普通用户文件的权限。普通用户将文件改成软连接指向root自身的某个文件。导致root实际修改了自身文件的权限。普通用户可将系统任意文件修改为特定权限。可对系统可用性造成影响。
防御:

  1. root不应修改普通用户文件的权限。
  2. root修改文件权限前必须校验不是软连接
  3. chown命令必须加-h选项
  4. root用chmod修改普通用户文件的权限时, 先切换到普通用户再做修改
  5. 明确文件owner,root修改时应校验文件owner为期望值

root写普通用户的文件,导致任意文件破坏

角色: root
原理: root写普通用户文件。普通用户将文件改成软连接指向系统任意文件。由于root可写任意文件,可导致任意文件被破坏。
防御:

  1. root写普通用户文件前必须校验文件是否为软连接。
  2. root写普通用户文件前必须校验文件owner是否是期望的owner

root写普通用户的目录中的某个文件,导致特定文件破坏

角色: root
原理: root到普通的目录写入某个文件。普通用户将目录软连到/etc,/usr等系统目录。可导致将特定文件放到任意位置,当特定文件与系统文件重名时,可破坏系统可用性
防御:

  1. root写普通用户文件前必须判断文件上层目录是否是软连接,需向上递归到root自己的目录为止

root执行普通用户的文件

角色: root
原理:

  1. root执行普通用户的脚本或者二进制
  2. root加载普通用户的动态库
  3. root解析普通用户的配置文件,并执行其中内容

防御:

  1. 禁止root执行普通用户的脚本或者二进制
  2. 禁止root加载普通用户的动态库
  3. 禁止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函数会一次性读入文件所有内容,不建议使用
posted @ 2022-11-21 14:43  易先讯  阅读(55)  评论(0编辑  收藏  举报