“scanf(“%d”,...)”和“gets”一样糟糕吗?

5
多年来,gets 一直被普遍认为是不安全的函数。(经典的 SO 问题是 为什么 gets 函数如此危险,以至于不能使用?)。gets 函数非常糟糕,以至于已从 C11 语言标准中删除。支持者(如果有的话)会争辩说,如果你了解输入的结构,那么使用它是完全可以的。
为什么那些贬低 gets 并承认依赖输入结构的愚蠢的人们允许使用 %d 作为 scanf 转换说明符呢?这是一个社会学问题,真正的问题是:为什么在 scanf 格式字符串中使用 %d 是不安全的?

1
实际上,控制台I/O和整个stdio.h都存在问题。没有GUI的程序应该从命令行参数和/或文件中获取输入。然后,对输入进行清理。如果在2022年左右的专业/商业环境中仍在使用1970年的失效库开发控制台I/O应用程序,他们应该认真退后并考虑自己在做什么。 - Lundin
1
我曾经遇到过同样的问题;原来是有反引号,但是有人编辑时把它们删除了,现在它们又回来了。 - Mustafa Aydın
1
@MustafaAydın - 很高兴知道我不是唯一一个 :) - ryyker
1
一个点击量很高的标题。当输入不经过清洗的时候,你总是可以从gets得到一个内存损坏,而scanf("%d", ...)最坏的情况只会给你一个未指定的整数(我知道理论上它是UB,但在实践中可能没有那么糟糕)。 - HolyBlackCat
这似乎很相关:scanf 的缺点 - Andrew Henle
显示剩余7条评论
4个回答

4
如果scanf的格式字符串包含一个原始的%d转换说明符(“原始”意思是“没有最大字段宽度”),如果输入流包含一个在int中无法容纳的整数的有效表示,那么行为是未定义的。例如,在sizeof(int)==4的平台上,字符串5294967296不能表示为int。语言C只保证int足够大以容纳-32767至+32767范围内的值,因此任何包含字符串32768的输入流都可能导致未定义的行为。可以通过使用%4d来避免这种潜在的未定义行为。现代大多数平台的INT_MAX的值远大于32767,因此转换说明符上的宽度修饰符可以比4更大,但它应该在编译时或运行时为平台确定,并且必须出现在格式字符串中。
如果您不添加宽度修饰符,您可以使用gets将一行读入缓冲区并使用sscanf解析值。这将使错误对读者更明显。

12
我认可你所描述的问题。但实际上,它在实践中并没有使用 gets() 那么有问题。 - John Bollinger
5
或者像 "%s" 这样没有宽度的一样糟糕。 - Retired Ninja
6
我觉得这个答案似乎在请求supercat发表关于C实现不会在出现未定义行为时任意选择做无意义的事情的评论。 - John Bollinger
顺便说一下,gets/sscanf的建议没有多少意义,是吧?从字符串或文件进行扫描在本质上并没有太大区别,尤其同样容易遭受溢出攻击。所需的是具有定义溢出行为的解析库。事实上,任何对不可信输入进行强大的输入处理都必须进行低级别的解析和错误处理/重新同步,即使这是痛苦和容易出错的。 - Peter - Reinstate Monica
1
@TobySpeight — §7.21.6.2 fscanf函数 ¶10: […] 如果该对象没有适当的类型,或者转换的结果无法在对象中表示,则行为是未定义的。 - Jonathan Leffler
显示剩余6条评论

4

不,scanf("%d", …)并不像gets那么糟糕。

gets是最糟糕的,因为在几乎任何环境下都无法安全使用。缓冲区溢出很可能发生,无法预防,并且很可能导致任意恶劣后果。

另一方面,scanf("%d", …)可能发生的最糟糕的事情是整数溢出。虽然这在理论上也是未定义行为,但在实践中,它几乎总是会导致(a)安静的回绕,(b)溢出到INT_MAXINT_MIN,或者(c)可能终止调用程序的运行时异常。

极难想象攻击者如何利用使用scanf("%d", …)的程序进行攻击。而涉及gets的攻击则非常普遍。

(虽然这不是问题的问法,但scanf("%s", …)gets一样危险。一个公平的问题是为什么前者没有像后者一样被强烈谴责。)


我一直在考虑将标题改为“为什么scanf(%d,...)是不安全的?”,但这样回答就会变得不那么相关,这很不幸,因为您提出了一个很好的观点。UB是UB,但(实际上)有些UB更糟糕。 - William Pursell
我已经从标题中删除了“为什么”,这使得您的第一段有点过时,但我认为保留了讨论的核心。非常感谢您的回答;您总是有很好的见解。 - William Pursell
@WilliamPursell 感谢您的提醒。我已经调整了我的第一段内容。 - Steve Summit

2

众所周知,旧版的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(),它总是只读取一行。


1
你可以为 scanf"%s" 参数传递一个大小(例如,"%8s" 或使用 scanf_s(),它允许/强制将其作为附加参数),已经可以使其读取和丢弃特定的空格 ("%*[ \t]"),并类似地仅读取单个换行符 ("%1[\n]")。glibc 实现还可以将 errno 设置为 ERANGE 以表示整数转换失败。 - Hasturkun
1
@Hasturkun:我同意你的观点,但我认为@chux想要更类似于使用“*”来指示长度由函数参数提供而不是需要在格式字符串中编码大小的printf() 符号。最好使用一种与POSIX兼容的%ms 符号的等效符号来动态分配字符串数据,最好使用不同的符号(因为%s 需要一个char *%ms 需要一个char **)。在理想的世界中,*会用于scanf()中的相同目的,但历史已经预先阻止了这个选项。 - Jonathan Leffler
也许 @ 字符(或其他未使用的字符)可以在 printf()scanf() 家族中都用于“长度来自参数”的情况。printf() 中的 * 表示可能会被弃用(标记为过时),但仍将无限期支持。scanf() 中的 * 仍将表示“不进行赋值”。 - Jonathan Leffler
我认为,在处理未知输入时,您总是需要一个自定义解析器;如果输入是基于行的,则可能需要使用正则表达式库。 (编写一个安全、方便的fgets()包装器,可靠地读取一行应该不太困难。)让scanf免受数字溢出攻击是很困难的:只有1个ungetc(),因此当数字太大时必然会丢失输入。 - Peter - Reinstate Monica
@Hasturkun 使用变量_width_ 强制改变格式 - 这是容易出错的,因为它需要 -1,并且防止了其他说明符的编译时检查。 scanf_s() 引入了约束处理,并且在 MS 与 C 规范之间关于大小类型不一致。 - chux - Reinstate Monica
@Hasturkun "%*[ \t]" 只会扫描一些非 '\n' 的空格。而且,空格的集合还与 locale 有关。"%1[\n]" 消耗一个 '\n' 并不太难,但对于弱库来说,在形成 scanset 方面可能会很昂贵。 - chux - Reinstate Monica

0

gets没有任何方法来防止缓冲区溢出错误。

对于scanf("%d", &x);,没有办法使缓冲区溢出错误(它的类型匹配格式字符串)。

现在,在以下情况下:

char s[5];
scanf("%s", s); 

存在缓冲区溢出的危险(当用户输入超过4个字符时),但是很容易修复此代码以保护免受缓冲区溢出:

char s[5];
scanf("%4s", s); 

现在这个版本不能发生缓冲区溢出。

请注意,scanf 存在中继漏洞,因此要防止与格式字符串相关的常见错误威胁警告。

基本上,gets 没有办法保护免受无效(过长)的用户输入。而且没有办法修复它,而不破坏二进制或源代码的兼容性。
在使用 scanf 的情况下,更高级的格式字符串可以保护您免受缓冲区溢出,并且可以通过静态分析工具强制执行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接