是 'scanf(“%d”, ...)“和”得到“一样糟糕?

Is `scanf("%d", ...)` as bad as `gets`?

提问人:William Pursell 提问时间:12/22/2022 最后编辑:William Pursell 更新时间:12/30/2022 访问量:603

问:

多年来,一直被普遍贬低为不安全的功能。(规范的 SO 问题是:为什么 gets 函数如此危险,以至于不应该使用它?该功能非常糟糕,以至于已从 C11 语言标准中删除。支持者(如果有的话)会争辩说,如果你知道输入的结构,使用它是完全可以的。getsgetsgets

为什么那些贬低并承认依赖输入结构是愚蠢的人允许使用作为转换说明符?这是一个社会学问题,真正的问题是:为什么格式字符串不安全?gets%dscanf%dscanf

c 扫描

评论

1赞 Lundin 12/22/2022
事实上,控制台 I/O 和 stdio.h 作为一个整体是有问题的。没有 GUI 的程序应该从命令行参数和/或文件中获取输入。然后从那里对输入进行清理。如果有人在 2022 年左右仍在专业/商业环境中使用 1970 年功能失调的库开发控制台 I/O 应用程序,他们应该认真地退后一步,考虑一下他们正在做什么。
0赞 ryyker 12/22/2022
顺便说一句,在我终于正确阅读之后,这是一个很好的问题!可能是因为一个常用的短语“......尽其所能。我看到了这个词,它没有,从而改变了我脑海中标题的全部含义。一旦我发现了我的错误,我就考虑编辑以使 gets 看起来像一个函数,但显然没有其他人遇到这个问题:)我责怪这个,或者这个
1赞 Mustafa Aydın 12/22/2022
我有同样的问题;事实证明,有反引号,但有人将它们编辑掉了,现在它们又回来了。
1赞 ryyker 12/22/2022
@MustafaAyd- 很高兴知道我不是唯一一个:)
1赞 HolyBlackCat 12/22/2022
点击诱饵标题。你总是可以在未经清理的输入上得到内存损坏,而最坏的情况是会给你一个未指定的整数(我知道它在理论上是UB,但在实践中可能没有那么糟糕)。getsscanf("%d", ...)

答:

4赞 William Pursell 12/22/2022 #1

如果格式字符串包含原始转换说明符(“raw”表示“没有最大字段宽度”),则如果输入流包含的字符串是无法放入 .例如,字符串不能在 的平台上表示,其中 .该语言仅保证 an 足够大以容纳范围 -32767 到 +32767,因此任何包含该字符串的输入流都可能导致未定义的行为。可以使用 来避免这种潜在的未定义行为。大多数现代平台的值 INT_MAX 远大于 32767,因此实际上转换说明符上的宽度修饰符可以大于 4,但应为平台确定(在编译时或运行时),并且它必须存在于格式字符串中。scanf%dint5294967296intsizeof(int) == 4Cint32768%4d

如果不添加宽度修饰符,则不妨只使用将一行读入缓冲区并用于解析值。这(也许)会使错误对读者来说更加明显。getssscanf

评论

12赞 John Bollinger 12/22/2022
我承认你所描述的问题。我否认在实践中它与使用 .gets()
5赞 Retired Ninja 12/22/2022
或者和没有宽度一样糟糕。"%s"
6赞 John Bollinger 12/22/2022
我觉得这个答案是在乞求 supercat 跳进来评论 C 实现,而不是在 UB 发生时随意选择做毫无意义的事情。
0赞 Peter - Reinstate Monica 12/22/2022
虽然最大字段宽度缓解了这个问题,但仍然容易受到 INT_MAX+1 等的影响,只要数字字符串适合,比如 10 位 32 位 int,不是吗?较新版本的标准应该至少定义实现,或者简单地定义它。scanf
1赞 Jonathan Leffler 12/23/2022
@TobySpeight — §7.21.6.2 fscanf 函数 ¶10[...]如果此对象没有适当的类型,或者转换结果无法在对象中表示,则行为未定义。
2赞 chux - Reinstate Monica 12/22/2022 #2

众所周知,前者无法控制/检测导致UB的缓冲液溢出。它本来可以有一个大小参数。gets()

除了@William Pursel关于范围的好答案。int

scanf(“%d”, ...): 输入不限于一行。

gets()阅读 1 。 在 中,首先使用可能包含多行的前导空格"%d"scanf()

scanf(“%d”, ...): 不读取整行。

与 不同,在输入后保留任何输入。这通常包括 .不阅读整行通常为后续问题埋下种子。gets()scanf("%d", ...)int'\n'

根据目标,不会抱怨尾随非数字文本。scanf("%d", ...)


C 语言缺乏一种强大的方法来读取一行。IMO、、、、扩展都缺少一些功能。fgets()gets_s()scanf(anything)getline()

我会争取一个总是读一行,总是形成一个字符串并返回(文件末尾,输入错误),成功时为 1,太小时为 0。int scan_line(size_t sz, char *buf /*, size_t *length_read*/)bufEOFsz


或者(更值得商榷)可以改进:*scanf()

  • 添加传递和朋友的能力。这是非常需要的。size"%s"

  • 定义了溢出时的行为。int

  • 类似于在空白处扫描,但不是.不影响返回值。"%#\n"'\n'

  • 类似于在 1 中扫描的东西。对返回值有贡献。可以使用前导空格来允许可选的前导非空格。"%\n"'\n'"% \n"'\n'

  • 报价始终只显示 1 行。*scanfln()

评论

1赞 Hasturkun 12/22/2022
您可以已经为参数传递大小(例如,,或使用 ,允许/强制它成为附加参数),已经可以使它读取并丢弃特定的空格 (),并且同样只读取单个换行符 ()。glibc 实现也可以设置为 for failed integer conversions。scanf"%s""%8s"scanf_s()"%*[ \t]""%1[\n]"errnoERANGE
1赞 Jonathan Leffler 12/23/2022
@Hasturkun:我同意你的看法——但我认为@chux想要更类似于用于指示长度由函数参数提供的符号,而不是要求在格式字符串中编码大小。与 POSIX 兼容的表示法等效,它为字符串动态分配数据——最好使用不同的表示法(因为期望一段时间需要 a)也是有益的。在理想情况下,将用于相同的目的,但历史已经抢占了这个选项。printf()*%ms%schar *%mschar ***scanf()
0赞 Jonathan Leffler 12/23/2022
也许这个角色(或一些未使用的字符)可以在两个家庭中使用,因为“长度来自争论”。中的表示法可能会被弃用(标记为过时),但将继续无限期地受到支持。in 仍然意味着“无分配”。@printf()scanf()*printf()*scanf()
0赞 Peter - Reinstate Monica 12/23/2022
我认为为了安全处理未知输入,您始终需要一个自定义解析器;如果输入是面向线的,也许正则表达式库会这样做。(围绕 fgets() 编写一个安全、方便的包装器来可靠地读取一行应该不会太难。很难针对数字溢出加强 scanf:只有 1 个 ungetc(),因此当数字太大时,输入总是会丢失。
0赞 chux - Reinstate Monica 12/23/2022
@Hasturkun 使用可变宽度需要重新格式 - 这很容易出错,并且会阻止对其他说明符进行编译时检查。 引入了约束处理,并且在 MS 与 C 规范之间关于大小类型的不一致。-1scanf_s()
4赞 Steve Summit 12/23/2022 #3

不,没有.scanf("%d", …)gets

gets因为它变得很糟糕,因为几乎在任何环境中都无法安全地使用它。缓冲区溢出是可能的,无法预防,并且很可能导致任意的不良后果。

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

很难想象攻击者可以使用 .另一方面,涉及的漏洞利用是司空见惯的。scanf("%d", …)gets

(虽然不是提出的问题,但确实与.这是一个公平的问题,为什么前者并不总是像后者那样受到贬低。scanf("%s", …)gets

评论

0赞 William Pursell 12/23/2022
我一直在争论将标题更改为“为什么是 scanf(”%d“, ...)不安全吗?“,但这个答案就不那么相关了,这很不幸,因为你提出了一个很好的观点。UB 就是 UB,但(在实践中)有些 UB 更糟。
0赞 William Pursell 12/23/2022
我从标题中删除了“为什么”,这使您的第一段有点过时,但我认为保留了讨论的核心。我真的很感谢你的回答;你总是有很好的洞察力。
0赞 Steve Summit 12/23/2022
@WilliamPursell 感谢您的提醒。我调整了我的第一段。
0赞 Marek R 12/23/2022 #4

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

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

现在,以防万一

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

存在缓冲区溢出的危险(当用户类型使用超过 4 个字符时),但修复此代码以防止缓冲区溢出很容易:

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

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

请注意,中继容易出错,因此请防止与格式字符串相关的常见错误威胁警告作为错误。scanf

基本上没有办法防止无效(到长)用户输入。此外,在不破坏二进制或源代码兼容性的情况下,也无法修复它。
如果是更高级的格式,字符串可以保护您的表单缓冲区溢出,这可以通过静态分析工具强制执行。
getsscanf