创建文件时必须显式指定合适的文件访问权限
【描述】 创建文件时,如果不显式指定合适访问权限,可能会让未经授权的用户访问该文件,造成信息泄露,文件数据被篡改,文件中被注入恶意代码等风险。
虽然文件的访问权限也依赖于文件系统,但是当前许多文件创建函数(例如POSIX open函数)都具有设置(或影响)文件访问权限的功能,所以当使用这些函数创建文件时,必须显式指定合适的文件访问权限,以防止意外访问。
【错误代码示例】 使用POSIX open()函数创建文件但未显示指定该文件的访问权限,可能会导致文件创建时具有过高的访问权限,这可能会导致漏洞。
void foo(void)
{
int fd = -1;
char *file_name = NULL;
... // 初始化 file_name
fd = open(file_name, O_CREAT | O_WRONLY); // 没有显式指定访问权限
if (fd == -1) {
... // 错误处理
}
...
}
【正确代码示例】 应该在open的第三个参数中显式指定新创建文件的访问权限。可以根据文件实际的应用情况设置何种访问权限。
void foo(void)
{
int fd = -1;
char *file_name = NULL;
... // 初始化 file_name 和指定其访问权限
// 此处根据文件实际需要,显式指定其访问权限
int fd = open(file_name, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR);
if (fd == -1) {
... // 错误处理
}
...
}
必须对文件路径进行规范化后再使用
【描述】 当文件路径来自外部数据时,必须对其做合法性校验,如果不校验,可能造成系统文件的被任意访问。但是禁止直接对其进行校验,正确做法是在校验之前必须对其进行路径规范化处理,因为: 同一个文件可以通过多种形式的路径来描述和引用,例如既可以是绝对路径,也可以是相对路径;而且路径名、目录名和文件名可能包含使校验变得困难和不准确的字符(如:"."、"..")。此外,文件还可以是符号链接,这进一步模糊了文件的实际位置或标识,增加了校验的难度和校验准确性。所以必须先将文件路径规范化,从而更容易校验其路径、目录或文件名,增加校验准确性,如使用realpath函数。
一个简单的案例说明如下:
当文件路径来自外部数据时,需要先将文件路径规范化,如果没有作规范化处理,攻击者就有机会通过恶意构造文件路径进行文件的越权访问。
例如,攻击者可以构造"../../../etc/passwd"的方式进行任意文件访问。
【错误代码示例】 在此错误的示例中,argv[1]包含一个源于受污染源的文件名,并且该文件名已打开以进行写入。在使用此文件名操作之前,应该对其进行验证,以确保它引用的是预期的有效文件。 不幸的是,argv[1]引用的文件名可能包含特殊字符,例如目录字符,这使验证变得困难,甚至不可能。而且,argv[1]中可能包含可以指向任意文件路径的符号链接,即使该文件名通过了验证,也会导致该文件名是无效的。 这种场景下,对文件名的直接验证即使被执行也是得不到预期的结果,对fopen()的调用可能会导致访问一个意外的文件。
...
if (!verify_file(input_file_name) { // 没有对input_file_name做规范化,直接做校验
... // 错误处理
}
if (fopen(input_file_name, "w") == NULL) {
... // 错误处理
}
...
【正确代码示例】 规范化文件名是具有一定难度的,因为这需要了解底层文件系统。 POSIX realpath()函数可以帮助将路径名转换为规范形式。
对上面的错误代码示例,我们采用如下解决方案:
char *real_path_res = NULL;
...
// 在校验之前,先对input_file_name做规范化处理
real_path_res = realpath(input_file_name, NULL);
if (real_path_res == NULL) {
... // 规范化的错误处理
}
// 规范化以后对路径进行校验
if (!verify_file(real_path_res) {
... // 校验的错误处理
}
// 使用
if (fopen(real_path_res, "w") == NULL) {
... // 实际操作的错误处理
}
...
free(real_path_res);
real_path_res = NULL;
...
【正确代码示例】 根据我们的实际场景,我们还可以采用的第二套解决方案,说明如下:
如果PATH_MAX被定义为中的一个常量,那么使用非空的resolved_path调用realpath()也是安全的。 在本例中realpath()函数期望resolved_path引用一个字符数组,该字符数组足够大,可以容纳规范化的路径。 如果定义了PATH_MAX,则分配一个大小为PATH_MAX的缓冲区来保存realpath()的结果。正确代码示例如下:
char *real_path_res = NULL;
char *canonical_file_name = NULL;
size_t path_size = 0;
...
path_size = (size_t)PATH_MAX;
if (verify_path_size(path_size) == TRUE) {
canonical_file_name = (char *)malloc(path_size);
if (canonical_file_name == NULL) {
... // 错误处理
}
real_path_res = realpath(inputFilename, canonical_file_name);
}
if (real_path_res == NULL) {
... // 错误处理
}
if (verify_file(real_path_res) == FALSE) {
... // 错误处理
}
if (fopen(real_path_res, "w") == NULL ) {
... // 错误处理
}
...
free(canonical_file_name);
canonical_file_name = NULL;
...
【错误代码示例】 下面的代码场景是从外部获取到文件名称,拼接成文件路径后,直接对文件内容进行读取,导致攻击者可以读取到任意文件的内容:
char *file_name = get_msg_from_remote();
...
sprintf(untrust_path, "/tmp/%s", file_name);
char *text = read_file_content(untrust_path);
【正确代码示例】 正确的做法是,对路径进行规范化后,再判断路径是否是本程序所认为的合法的路径:
char *file_name = get_msg_from_remote();
...
sprintf(untrust_path, "/tmp/%s", file_name);
char path[PATH_MAX] = {0};
if (realpath(untrust_path, path) == NULL) {
... // 处理错误
}
if (!is_valid_path(path)) { // 检查文件的位置是否正确
... // 处理错误
}
char *text = read_file_content(path);
【例外】
运行于控制台的命令行程序,通过控制台手工输入文件路径,可以作为本条款例外。
int main(int argc, char **argv)
{
int fd = -1;
if (argc == 2) {
fd = open(argv[1], O_RDONLY);
...
}
...
}
不要在共享目录中创建临时文件
【描述】 程序的临时文件应当是程序自身独享的,任何将自身临时文件置于共享目录的做法,将导致其他共享用户获得该程序的额外信息,产生信息泄露。因此,不要在任何共享目录创建仅由程序自身使用的临时文件。
程序员通常会在共享目录中(例如在/tmp和/var/tmp创建临时文件,并且还有可能会定期清除这些临时文件(例如,每晚或重新启动期间),但也可能不注意清理。
临时文件通常用于辅助保存不能驻留在内存中的数据或存储临时的数据,也可用作进程间通信的一种手段(通过文件系统传输数据)。例如,一个进程在共享目录中创建一个临时文件,该文件名可能使用了众所周知的名称或者一个临时的名称,然后就可以通过该文件在进程间共享信息。这种通过在共享目录中创建临时文件的方法实现进程间共享的做法很危险,因为共享目录中的这些文件很容易被攻击者劫持或操纵。这里有几种缓解策略:
-
1.使用其他低级IPC(进程间通信)机制,例如套接字或共享内存。
-
2.使用更高级别的IPC机制,例如远程过程调用。
-
3.使用仅能由程序本身访问的安全目录(多线程/进程下注意防止条件竞争)。
同时,下面列出了几项临时文件创建使用的方法,产品根据具体场景执行以下一项或者几项,同时产品也可以自定义合适的方法。
-
1.文件必须具有合适的权限,只有符合权限的用户才能访问
-
2.创建的文件名是唯一的、或不可预测的
-
3.仅当文件不存在时才创建打开(原子创建打开)
-
4.使用独占访问打开,避免竞争条件
-
5.在程序退出之前移除
同时也需要注意到,当某个目录被开放读/写权限给多个用户或者一组用户时,该共享目录潜在的安全风险远远大于访问该目录中临时文件这个功能的本身。
如果想安全地在共享目录中创建临时文件,而不受威胁是不容易的。例如,用于本地挂载的文件系统的代码在与远程挂载的文件系统一起共享使用时可能会受到攻击。而且上面的函数安全版本还受限于所使用的C运行时库、操作系统和文件系统的版本。唯一安全的解决方案是不要在共享目录中创建临时文件。
【错误代码示例】 如下代码示例,程序在Linux系统的共享目录/tmp下创建临时文件来保存临时数据,且文件名是硬编码的。 由于文件名是硬编码的,因此是可预测的,攻击者只需用符号链接替换文件,然后链接所引用的目标文件就会被打开并写入新内容。
void proc_data(const char *file_name)
{
FILE *fp = fopen(file_name, "wb+");
if (fp == NULL) {
... // 错误处理
}
... // 写文件
fclose(fp);
}
int main(void)
{
// 不合规:1.在系统共享目录中创建临时文件;2.临时文件名硬编码
char *real_file = "/tmp/data";
...
proc_data(real_file);
...
return 0;
}
【正确案例】
Linux下的/tmp目录是一个所有用户都可以访问的共享目录,不应在该目录下创建仅由程序自身使用的临时文件。
【业界典型漏洞】CVE-2004-2502