我正在尝试编写一个安全版本的fopen,以防止打开符号链接的文件。以下是我摘自here的代码。
/*
Secure fopen
*/
enum { FILE_MODE = 0600 };
FILE *secure_fopen(char *filename, char* mode)
{
int fd;
FILE *f;
unlink(filename);
if (strncmp(mode, "w", 1) == 0) {
fd = open(filename, O_WRONLY|O_CREAT|O_EXCL, FILE_MODE);
}
else if (strncmp(mode, "r", 1) == 0) {
fd = open(filename, O_RDONLY|O_CREAT|O_EXCL);
}
else {
fd = open(filename, O_RDWR|O_CREAT|O_EXCL, FILE_MODE);
}
if (fd == -1) {
perror("Failed to open the file");
return NULL;
}
/* Get a FILE*, as they are easier and more efficient than file descriptors */
f = fdopen(fd, mode);
if (f == NULL) {
perror("Failed to associate file descriptor with a stream");
return NULL;
}
return f;
}
此代码有两个问题:一-它覆盖文件名所指向的文件,二-返回NULL,但是NULL文件指针不会在最终检查中被捕获:
if (f == NULL) {
perror("Failed to associate file descriptor with a stream");
return NULL;
}
有人对这两种情况的发生有任何见识吗?
答案 0 :(得分:2)
首先,O_CREAT
创建一个文件(如果它不存在),而O_CREAT|O_EXCL
创建一个文件,如果它已经不存在,则失败。
第二,(strncmp(mode, "w", 1) == 0)
等效于(mode[0] == 'w')
,这可能不是您想要的。您可能是用(strchr(mode, "w"))
来代替的。
请考虑以下实现(完整的示例程序):
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* Internal flags used by custom_fopen(): */
#define FM_R (1<<0) /* r, w+: For reading */
#define FM_W (1<<1) /* w, r+: For writing */
#define FM_TRUNC (1<<2) /* w, w+: Truncate */
#define FM_CREAT (1<<3) /* w, r+: Create if necessary */
#define FM_EXCL (1<<4) /* x: Fail if already exists */
#define FM_APPEND (1<<5) /* a: Append */
#define FM_CLOEXEC (1<<6) /* e: Close-on-exec() */
#define FM_SYMLINK (1<<7) /* s: Fail if last path component is a symlink */
#define FM_RW (FM_R | FM_W) /* r+, w+ */
FILE *custom_fopen(const char *path, const char *mode)
{
const char *fdmode;
int fm, flags, fd, saved_errno;
FILE *ret;
if (!path || !*path || !mode) {
errno = EINVAL;
return NULL;
}
switch ((strchr(mode, 'r') ? 1 : 0) +
(strchr(mode, 'w') ? 2 : 0) +
(strchr(mode, 'a') ? 4 : 0) +
(strchr(mode, '+') ? 8 : 0)) {
case 1: fdmode = "r"; fm = FM_R; break;
case 2: fdmode = "w"; fm = FM_W | FM_CREAT | FM_TRUNC; break;
case 4: fdmode = "a"; fm = FM_W | FM_CREAT | FM_APPEND; break;
case 9: fdmode = "r+"; fm = FM_RW | FM_CREAT; break;
case 10: fdmode = "w+"; fm = FM_RW | FM_CREAT | FM_TRUNC; break;
case 12: fdmode = "a+"; fm = FM_RW | FM_CREAT | FM_APPEND; break;
default:
/* Invalid combination of 'r', 'w', 'a', and '+'. */
errno = EINVAL;
return NULL;
}
if (strchr(mode, 'x')) {
if (fm & FM_CREAT)
fm |= FM_EXCL;
else {
/* 'rx' does not make sense, and would not work anyway. */
errno = EINVAL;
return NULL;
}
}
if (strchr(mode, 'e'))
fm |= FM_CLOEXEC;
if (strchr(mode, 's'))
fm |= FM_SYMLINK;
/* Verify 'mode' consists of supported characters only. */
if (strlen(mode) != strspn(mode, "rwa+xesb")) {
errno = EINVAL;
return NULL;
}
/* Map 'fm' to 'flags' for open(). */
switch (fm & FM_RW) {
case FM_R: flags = O_RDONLY; break;
case FM_W: flags = O_WRONLY; break;
case FM_RW: flags = O_RDWR; break;
default:
errno = EINVAL;
return NULL;
}
if (fm & FM_TRUNC) flags |= O_TRUNC;
if (fm & FM_CREAT) flags |= O_CREAT;
if (fm & FM_EXCL) flags |= O_EXCL;
if (fm & FM_APPEND) flags |= O_APPEND;
if (fm & FM_CLOEXEC) flags |= O_CLOEXEC;
if (fm & FM_SYMLINK) flags |= O_NOFOLLOW;
/* Open the file. If we might create it, use mode 0666 like fopen() does. */
if (fm & FM_CREAT)
fd = open(path, flags, 0666);
else
fd = open(path, flags);
/* Failed? */
if (fd == -1)
return NULL; /* errno set by open() */
/* Convert the file descriptor to a file handle. */
ret = fdopen(fd, fdmode);
if (ret)
return ret;
/* Failed. Remember the reason for the failure. */
saved_errno = errno;
/* If we created or truncated the file, unlink it. */
if (fm & (FM_EXCL | FM_TRUNC))
unlink(path);
/* Close the file descriptor. */
close(fd);
/* Return, recalling the reason for the failure. */
errno = saved_errno;
return NULL;
}
int main(int argc, char *argv[])
{
FILE *handle;
if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s PATH MODE\n", argv[0]);
fprintf(stderr, "\n");
return EXIT_FAILURE;
}
handle = custom_fopen(argv[1], argv[2]);
if (!handle) {
const int err = errno;
fprintf(stderr, "custom_fopen(\"%s\", \"%s\") == NULL, errno = %d: %s.\n",
argv[1], argv[2], err, strerror(err));
return EXIT_FAILURE;
}
if (fclose(handle)) {
const int err = errno;
fprintf(stderr, "fclose(custom_fopen(\"%s\", \"%s\")) failed, errno = %d: %s.\n",
argv[1], argv[2], err, strerror(err));
return EXIT_FAILURE;
}
printf("custom_fopen(\"%s\", \"%s\"): Success.\n", argv[1], argv[2]);
return EXIT_SUCCESS;
}
#define _POSIX_C_SOURCE 200809L
告诉您的C库(至少与GNU C兼容的C库)公开POSIX.1-2008功能(如open()
)。
r
,w
,a
,r+
,w+
和a+
模式的行为如{{3 }}。其中至少一个必须位于mode
中。 (不过,+
不需要立即跟在字母后面。)
上述实现还支持b
(对每个POSIX不执行任何操作),x
(如果创建新文件但已存在,则失败),s
(如果文件名部分不成功)的路径是符号链接)和e
(在exec处关闭描述符,而不是将其泄漏给子进程)。
第一个switch
语句处理主要模式,而忽略字符顺序。它实际上检查rwa+
中四个字符mode
中的哪个,并且仅接受理智的组合。仅当mode
包含'c'
时,man 3 fopen
调用返回非零指针(逻辑为真)。
它后面的if
子句在模式下检测到x
。不允许将x
和r
组合使用,因为这是没有意义的。 (POSIX.1表示open()
与O_RDONLY | O_EXCL
的行为是不确定的。)
(strlen(mode) == strspn(mode, "rwa+xesb"))
验证mode
仅包含字母r
,w
,a
,+
,x
, e
,s
和b
;它们可以重复,也可以以任何顺序重复。此检查拒绝不支持的字符。
第二条switch
语句和if
子句将fm
映射到flags
。我们这样做是因为O_
常量可能不是单个位,这意味着诸如(flags & O_RDONLY)
,(flags & O_WRONLY)
和(flags & O_RDWR)
之类的测试不可靠,实际上将不起作用人们可能期望的方式。相反,我们使用fm
和我们自己的单一位FM_
常量(可以将其视为掩码),然后稍后将它们映射到flags
的相应值。 (简而言之,fm
跟踪我们想要的功能,并且我们仅将相应的标志集分配给flags
,而不会检查 flags
。)>
如果我们可以创建文件,请使用模式0666
(rw-rw-rw-
),该模式由用户的umask
修改。 (通常,普通用户的umask为002
,007
,022
或077
,这导致新文件的模式为0664
({{1 }}},rw-rw-r--
(0660
),rw-rw----
(0644
)或rw-r--r--
(0600
)。
这正是rw-------
所做的。
拥有打开的文件描述符后,我们仍然必须将其与流句柄关联。我们使用strchr(mode, 'c')
完成。请注意,我们在第一个fopen()
语句中为此调用确定了正确的mode
。如果此调用成功,则流将“拥有”文件描述符,而我们要做的就是返回返回的流句柄switch
。
如果fdopen()
失败,我们需要关闭文件描述符。我们也可能决定删除/取消链接文件。如果我们确定文件以前不存在(fdopen()
),或者如果我们将其截断了(e
,w
),那么上面的代码将删除该文件,因为该文件可能包含的数据反正迷路了。
测试程序采用两个命令行参数:路径或文件名以及模式字符串。该程序进行w+
调用,并报告结果。 (如果custom_fopen(pathorfilename, modestring)
调用成功,它还会检查相应的custom_fopen()
调用是否成功,因为有时与文件描述符有关的问题(或fclose()
/ {{1}的不兼容模式标志}调用)只能在完成的第一个操作或流关闭时观察到。
我仅对上述功能和程序进行了少量测试,并且总是可能出现错误。如果您发现错误或代码有问题,请在评论中告知我,以便在必要时进行验证和修复。