gets
一直被普遍认为是不安全的函数。(经典的 SO 问题是 为什么 gets 函数如此危险,以至于不能使用?)。gets
函数非常糟糕,以至于已从 C11 语言标准中删除。支持者(如果有的话)会争辩说,如果你了解输入的结构,那么使用它是完全可以的。为什么那些贬低
gets
并承认依赖输入结构的愚蠢的人们允许使用 %d
作为 scanf
转换说明符呢?这是一个社会学问题,真正的问题是:为什么在 scanf
格式字符串中使用 %d
是不安全的?gets
一直被普遍认为是不安全的函数。(经典的 SO 问题是 为什么 gets 函数如此危险,以至于不能使用?)。gets
函数非常糟糕,以至于已从 C11 语言标准中删除。支持者(如果有的话)会争辩说,如果你了解输入的结构,那么使用它是完全可以的。gets
并承认依赖输入结构的愚蠢的人们允许使用 %d
作为 scanf
转换说明符呢?这是一个社会学问题,真正的问题是:为什么在 scanf
格式字符串中使用 %d
是不安全的?scanf
的格式字符串包含一个原始的%d
转换说明符(“原始”意思是“没有最大字段宽度”),如果输入流包含一个在int
中无法容纳的整数的有效表示,那么行为是未定义的。例如,在sizeof(int)==4
的平台上,字符串5294967296
不能表示为int
。语言C只保证int
足够大以容纳-32767至+32767范围内的值,因此任何包含字符串32768
的输入流都可能导致未定义的行为。可以通过使用%4d
来避免这种潜在的未定义行为。现代大多数平台的INT_MAX的值远大于32767,因此转换说明符上的宽度修饰符可以比4更大,但它应该在编译时或运行时为平台确定,并且必须出现在格式字符串中。gets
将一行读入缓冲区并使用sscanf
解析值。这将使错误对读者更明显。gets()
那么有问题。 - John Bollinger"%s"
这样没有宽度的一样糟糕。 - Retired Ninjafscanf
函数 ¶10: […] 如果该对象没有适当的类型,或者转换的结果无法在对象中表示,则行为是未定义的。 - Jonathan Leffler不,scanf("%d", …)
并不像gets
那么糟糕。
gets
是最糟糕的,因为在几乎任何环境下都无法安全使用。缓冲区溢出很可能发生,无法预防,并且很可能导致任意恶劣后果。
另一方面,scanf("%d", …)
可能发生的最糟糕的事情是整数溢出。虽然这在理论上也是未定义行为,但在实践中,它几乎总是会导致(a)安静的回绕,(b)溢出到INT_MAX
或INT_MIN
,或者(c)可能终止调用程序的运行时异常。
极难想象攻击者如何利用使用scanf("%d", …)
的程序进行攻击。而涉及gets
的攻击则非常普遍。
(虽然这不是问题的问法,但scanf("%s", …)
与gets
一样危险。一个公平的问题是为什么前者没有像后者一样被强烈谴责。)
众所周知,旧版的gets()
无法控制/检测缓冲区溢出,导致UB。如果它可以有一个size参数,就可以解决这个问题。
除了@William Pursel关于int
范围的好答案之外,还有以下补充:
scanf("%d", ...)
: 输入不限于一行。
gets()
只读取1行。而scanf()
中的"%d"
则首先消耗前导空格,可能包括多行。
scanf("%d", ...)
: 不会读取整行。
与gets()
不同,scanf("%d", ...)
在读取int
之后会保留其余输入。这通常包括一个'\n'
。未读取整行经常会引发后续问题。
根据目标不同,scanf("%d", ...)
不会抱怨尾随的非数字文本。
C语言缺乏一种强大的读取行的方式。在我看来,fgets()
、gets_s()
、scanf(anything)
和扩展getline()
都缺少一些功能。
我会倡导一个int scan_line(size_t sz, char *buf /*, size_t *length_read*/)
函数,它总是读取一行,总是在buf
中形成一个字符串,并返回EOF
(文件结束,输入错误),成功时返回1,当sz
太小时返回0。
或者(更值得商榷的是)*scanf()
可以改进:
为 "%s"
等添加传递size
的能力。这极为需要。
对于 int
溢出有明确定义的行为。
类似 "%#\n"
,可以扫描空格,但不包括'\n'
。不会对返回值做出贡献。
类似 "%\n"
,可以扫描1个'\n'
。对返回值做出贡献。可以使用前导空格 "% \n"
来允许可选的前导非'\n'
的空格。
提供 *scanfln()
,它总是只读取一行。
scanf
的 "%s"
参数传递一个大小(例如,"%8s"
或使用 scanf_s()
,它允许/强制将其作为附加参数),已经可以使其读取和丢弃特定的空格 ("%*[ \t]"
),并类似地仅读取单个换行符 ("%1[\n]"
)。glibc 实现还可以将 errno
设置为 ERANGE
以表示整数转换失败。 - Hasturkunprintf()
符号。最好使用一种与POSIX兼容的%ms
符号的等效符号来动态分配字符串数据,最好使用不同的符号(因为%s
需要一个char *
而%ms
需要一个char **
)。在理想的世界中,*
会用于scanf()
中的相同目的,但历史已经预先阻止了这个选项。 - Jonathan Leffler@
字符(或其他未使用的字符)可以在 printf()
和 scanf()
家族中都用于“长度来自参数”的情况。printf()
中的 *
表示可能会被弃用(标记为过时),但仍将无限期支持。scanf()
中的 *
仍将表示“不进行赋值”。 - Jonathan Leffler-1
,并且防止了其他说明符的编译时检查。 scanf_s()
引入了约束处理,并且在 MS 与 C 规范之间关于大小类型不一致。 - chux - Reinstate Monica"%*[ \t]"
只会扫描一些非 '\n'
的空格。而且,空格的集合还与 locale 有关。"%1[\n]"
消耗一个 '\n'
并不太难,但对于弱库来说,在形成 scanset 方面可能会很昂贵。 - chux - Reinstate Monicagets
没有任何方法来防止缓冲区溢出错误。
对于scanf("%d", &x);
,没有办法使缓冲区溢出错误(它的类型匹配格式字符串)。
现在,在以下情况下:
char s[5];
scanf("%s", s);
存在缓冲区溢出的危险(当用户输入超过4个字符时),但是很容易修复此代码以保护免受缓冲区溢出:
char s[5];
scanf("%4s", s);
现在这个版本不能发生缓冲区溢出。
请注意,scanf
存在中继漏洞,因此要防止与格式字符串相关的常见错误威胁警告。
基本上,gets
没有办法保护免受无效(过长)的用户输入。而且没有办法修复它,而不破坏二进制或源代码的兼容性。
在使用 scanf
的情况下,更高级的格式字符串可以保护您免受缓冲区溢出,并且可以通过静态分析工具强制执行。
gets
得到一个内存损坏,而scanf("%d", ...)
最坏的情况只会给你一个未指定的整数(我知道理论上它是UB,但在实践中可能没有那么糟糕)。 - HolyBlackCat