调用格式化输入/输出函数时,使用有效的格式字符串

【描述】

格式化输入/输出函数(如fscanf()/fprintf()及相关函数)在format字符串控制下进行转换、格式化、打印其实参。

在创建格式化字符串时的常见错误包括:

  • format中参数个数与实参个数不一致;

  • 使用无效的转换指示符;

  • 使用与转换指示符不兼容的标志字符;

  • 使用与转换指示符不兼容的长度修饰符;

  • format中转换指示符与实参类型不匹配;

  • 使用int以外类型的实参指定宽度或者精度;

不要为格式化输入/输出函数提供未知的或者无效的转换规格,以及标志字符、精度、长度修饰符、转换指示符的无效组合。同样,不要提供与格式化字符串中的转换指示符类型不匹配的实参。这可能会使程序产生未定义行为。

【错误代码示例】

如下代码示例中,printf()的实参infoLevel类型与对应的转换指示符 's '不匹配,正确的转换指示符要使用 'd '。同样,实参infoMsg类型与对应的转换指示符 'd '不匹配,正确的转换指示符要使用 's '。 这些用法会使程序产生未定义行为,比如:printf()将把infoLevel实参解释为指针,试图从infoLevel包含的地址中读取一个字符串,从而发生非法访问。

void foo(void)
{
	const char *info_msg = "Information seed to user.";
	int info_level = 3;
	...
	printf("infoLevel: %s, infoMsg: %d\n", info_level, info_msg);
	...
}

【正确代码示例】

正确的做法是确保printf()函数的实参匹配format的转换指示符。

void foo(void)
{
	const char *info_msg = "Information seed to user.";
	int info_level = 3;
	...

	printf("infoLevel: %d, infoMsg: %s\n", info_level, info_msg);
	...
}

【影响】

错误的格式串可能造成内存破坏或者程序异常终止。

禁止使用alloca()函数申请栈上内存

【描述】 POSIX和C99均未定义alloca()的行为,在有些平台下不支持该函数,使用alloca会降低程序的兼容性和可移植性,该函数在栈帧里申请内存,申请的大小很可能超过栈的边界,影响后续的代码执行。

请使用malloc从堆中动态分配内存。

【影响】

程序栈的大小非常有限,如果分配导致栈溢出,则程序产生未定义行

禁止使用realloc()函数

【描述】 realloc()是一个非常特殊的函数,原型如下:

void *realloc(void *ptr, size_t size);

随着参数的不同,其行为也是不同:

  • 当ptr不为NULL,且size不为0时,该函数会重新调整内存大小,并将新的内存指针返回,并保证最小的size的内容不变;

  • 参数ptr为NULL,但size不为0,那么其行为等同于malloc(size);

  • 参数size为0,则realloc的行为等同于free(ptr)。

由此可见,一个简单的C函数,却被赋予了3种行为,这不是一个设计良好的函数。虽然在编程中提供了一些便利性,如果认识不足,使用不当,是却极易引发各种bug。

【错误代码示例】 如下代码示例中,使用realloc不当导致内存泄漏。 代码中希望对ptr的空间进行扩充,当realloc()分配失败的时候,会返回NULL。但是参数中的ptr的内存是没有被释放的,如果直接将realloc()的返回值赋给ptr,那么ptr原来指向的内存就会丢失,造成内存泄漏。

// 当realloc()分配内存失败时会返回NULL,导致内存泄漏
char *ptr = (char  *)realloc(ptr, NEW_SIZE);
if (ptr == NULL) {
	...// 错误处理
}

【正确代码示例】 使用malloc()函数代替realloc()函数。

// 使用malloc()函数代替realloc()函数
char *new_ptr = (char *)malloc(NEW_SIZE);
if (new_ptr == NULL) {
	... // 错误处理
}

(void)memcpy(new_ptr, old_ptr, old_size);
... // 返回前,释放old_Ptr

【影响】

使用不当容易造成内存泄漏和双重释放问题。不正确的内存对齐可能导致对象访问异常。

禁止外部可控数据作为进程启动函数的参数

【描述】 本条款中进程启动函数包括system、popen、execl、execlp、execle、execv、execvp等。

system()、popen()等函数会创建一个新的进程,如果外部可控数据作为这些函数的参数,会导致注入漏洞。

使用execl()、execlp()等函数执行新进程时,如果使用shell启动的新进程,则同样存在命令注入风险。

因此,总是优先考虑使用C标准函数实现需要的功能。如果确实需要使用这些函数,请使用白名单机制确保这些函数的参数不受任何外来数据的影响。

【错误代码示例】 如下代码示例中,使用 system() 函数执行 cmd 命令串来自外部,攻击者可以执行任意命令:

char  *cmd = get_cmd_from_remote();
if (cmd == NULL) {
	... // 处理错误
}

system(cmd);

如下代码示例中,使用 system() 函数执行 cmd 命令串的一部分来自外部,攻击者可能输入 'some dir;useradd xxx '字符串,创建一个xxx的用户:

char cmd[MAX_LEN ];
int ret;

char  *name = get_dir_name_from_remote();
if (name == NULL) {
	... // 处理错误
}

ret = sprintf(cmd, "ls %s", name);
...
system(cmd);

使用exec系列函数来避免命令注入时,注意exec系列函数中的path、file参数禁止使用命令解析器(如/bin/sh)。

int execl(const char *path, const char *arg,  ...);
int execlp(const char *file, const char *arg,  ...);
int execle(const char *path, const char *arg,  ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

例如,禁止如下使用方式:

char *cmd = get_dir_name_from_remote();
execl("/bin/sh", "sh",  "-c", cmd, NULL);

【正确代码示例】 (使用库函数)

在Linux下实现对当前目录下文件名的打印,可以使用opendir(), readdir(), stat()等函数直接实现ls-l命令的功能,不必使用system()函数。下面是一个简化的ls -l示例版本,列出一个由程序内部指定的文件的信息,该函数仅考虑了不需要重入的情况。

static int OutputFileInfo(const char *file_name)
{
	const char priv[] = {'x', 'w', 'r'};
	ASSERT(file_name != NULL);

	struct stat st;
	int ret = stat(file_name, &st);
	if (ret == -1) {
		return -1;
	}

	const struct passwd *pw = getpwuid(st.st_uid);
	if (pw == NULL) {
		return -1;
	}

	const struct group *gp = getgrgid(st.st_gid);
	if (gp == NULL) {
		return -1;
	}

	if (S_ISREG(st.st_mode)) {
		printf("-");
	} else if (S_ISDIR(st.st_mode)) {
		printf("d");
	}

	for (int i = 8; i >= 0; i --) {
		if ((st.st_mode & (1 < < i)) != 0) {
			printf("%c", priv[i % 3]);
		} else {
			printf("-");
		}
	}

	printf("%s %s %ld %s\n",
	pw->pw_name,
	gp->gr_name,
	st.st_size,
	file_name);
	return 0;
}

【正确代码示例】 (使用exec系列函数)

可以通过库函数简单实现的功能(如上例),需要避免调用命令处理器来执行外部命令。如果确实需要调用单个命令,应使用exec *函数来实现参数化调用,并对调用的命令实施白名单管理。

pid_t pid;
char * const envp[] = { NULL };
...
char *file_name = get_dir_name_from_remote();
if (file_name == NULL) {
	... // 处理错误
}
...
if ((pid = fork()) < 0) {
	...
} else if (pid == 0) {
	// 使用some_tool对指定文件进行加工
	execle( "/bin/some_tool", "some_tool", file_name, NULL, envp);
	_Exit(-1);
}

...
int status;
waitpid(pid, &status, 0);
FILE *fp = fopen(file_name, "r");
...

此时,外部输入的file_name仅作为some_tool命令的参数,没有命令注入的风险。

【正确代码示例】 (使用白名单)

对输入的文件名基于合理的白名单检查,避免命令注入。

char *cmd = get_cmd_from_remote();
if (cmd == NULL) {
	... // 处理错误
}

// 使用白名单检查命令是否合法,仅允许 "some_tool_a", "some_tool_b"命令,外部无法随意控制
if (!is_valid_cmd(cmd)) {
	... // 处理错误
}

system(cmd);
...

【相关软件CWE编号】 CWE-676,CWE-88

禁止在信号处理例程中调用非异步安全函数

【描述】 在信号处理程序中只调用异步安全函数。

除了C语言标准函数以外,其他系统函数也提供了一些的异步安全函数,在信号处理程序中使用这些函数之前,应确保调用的函数在所有可能的执行环境下均是异步安全的。

【错误代码示例】 如下代码示例中,信号处理函数中调用了非异步安全函数printf():

void handler(int num)
{
	printf("receive signal = %d \n", SIGINT);
}

int main(int argc, char **argv)
{
	if (signal(SIGINT, handler) == SIG_ERR) {
 		... // 错误处理
	}

	while (true) {
 		... // 程序主循环代码
	}

	return 0;
}

【正确代码示例】 如下代码示例中,尽量不在信号处理函数中调用其他函数,仅在信号处理程序中修改volatile sig_atomic_t类型的变量:

static volatile sig_atomic_t g_flag = 0;
void handler(int num)
{
	g_flag = 1;
}

int main(int argc, char **argv)
{
	if (signal(SIGINT, handler) == SIG_ERR) {
	... // 错误处理
	}

	while (true) {
		if (g_flag != 0) {
			printf("receive signal = %d\n", SIGINT);
		}

		... // 程序主循环代码
	}

	...
	return 0;
}

【相关软件CWE编号】 CWE-479