vsnprintf()
是一个完美的标准函数,如果使用得当,没有安全问题,特别是如果格式字符串是与传递的参数兼容的字符串文字。
Microsoft 已弃用此功能,因为攻击者可以利用使用用户提供的文本作为格式规范的草率代码:精心制作的用户提供的字符串可以使用该%n
格式来尝试破坏程序的数据并使程序执行任意代码。
使用可变格式字符串容易出错,因为很难验证参数的类型是否与此动态生成的格式字符串兼容。合理的编码约定不允许这样的代码。将用户提供的字符串作为格式字符串传递是完全不合适的,因为它是未定义行为的微不足道的来源。禁用有用的和标准的功能,因为它们可能被滥用并造成安全漏洞,这是值得称赞的,但在这种特殊情况下,建议的替代方案存在缺陷。
有 2 个替代方案vsnprintf()
,在附件 K 中标准化,但具有可选支持,因此不可跨环境移植:
int vsprintf_s(char * restrict s, rsize_t n,
const char * restrict format,
va_list arg);
和
int vsnprintf_s(char * restrict s, rsize_t n,
const char * restrict format,
va_list arg);
这些函数的行为与snprintf
微妙的方式不同:
- 都不支持说明
%n
符。
- 两者都不允许 size 参数
n
有0
值。
- 两者都不允许
s
目标数组为空指针,snprintf()
如果n
大小为0
.
vsprintf_s
与vsnprintf_s
输出字符串长于时不同n-1
:它将目标设置为空字符串,调用错误处理程序并返回0
一个负数,而不是格式化字符串的长度,就像两者vsnprintf
一样vsnprintf_s
。
您没有发布要替换的代码snprintf()
,但一个经典的用例是这样的:
/* allocate a string formated according to `format` */
int vasprintf(char **strp, const char *format, va_list ap) {
va_list arg;
int ret;
if (!strp) {
errno = EINVAL;
return -1;
}
va_copy(arg, ap);
ret = vsnprintf(NULL, 0, format, arg);
va_end(arg);
*strp = NULL;
if (ret < 0 || (*strp = malloc(ret + 1)) == NULL) {
return -1;
}
return vsnprintf(*strp, ret + 1, format, ap);
}
在上面的代码中,vnsprintf
不能替换为vsprintf_s
因为vsprintf_s
不能用于计算所需的大小:它总是将短大小视为错误。vsnprintf_s
不能用作直接替换,因为它认为NULL
指针作为目标也是错误的。
解决方案是:替换vsnprintf
为vsnprintf_s
并传递一个大小为的短本地缓冲区,1
而不是NULL, 0
:
/* allocate a string formated according to `format` */
int vasprintf(char **strp, const char *format, va_list ap) {
char buf[1];
va_list arg;
int ret;
if (!strp) {
errno = EINVAL;
return -1;
}
va_copy(arg, ap);
ret = vsnprintf_s(buf, 1, format, arg);
va_end(arg);
*strp = NULL;
if (ret < 0 || (*strp = malloc(ret + 1)) == NULL) {
return -1;
}
return vsnprintf_s(*strp, ret + 1, format, ap);
}
请注意,还有另一个更简单的解决方案:您可以通过在有问题的代码之前定义一个宏来防止 Visual Studio 发出此警告:
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
这可以防止对所有已弃用的库函数发出警告。
另请注意,提高警告级别并让编译器警告潜在的错误非常有用,例如给定格式字符串的参数不一致。使用gcc
and clang
,您可以传递-Wall
和其他命令行选项来启用此类行为。对于 Visual Studio,您可以使用/W4
.