从stdin中使用fgets读取,如果输入大于缓冲区大小,则存在漏洞。

4
我正在创建一个带提示符的命令行应用程序。只要输入内容适合缓冲区,它就能正常工作,但当输入内容较大时,会出现一些奇怪的行为。以下是一个简化的最小示例,具有相同的错误。
int main()
{
    for (;;) {
        const int size = 8;
        char str[size];

        printf("> ");
        fgets(str, size, stdin);

        toupper_str(str);
        printf("Result: %s\n", str);
    }
}

如果输入小于size,它就能正常工作。

> asdf
Result: ASDF
> 

当输入内容较大时,处理的是范围内的输入内容,并在下一次循环迭代中,余下的输入内容将立即从 fgets 返回。这会导致该部分输入内容也被处理并产生一些奇怪的输出。
> asdfghjk
Result: ASDFGHJ
> Result: K

> 

我可以通过比较最后一个字符与换行符来判断输入是否大于或等于指定大小。只要符合条件,fgets 就会保留换行符。

fgets(str, size, stdin);
if (str[strlen(str) - 1] != '\n') {
    fprintf(stderr, "Input too long\n");
}

当检测到这种情况时,如何防止它在下一次迭代中读取过长的输入的其余部分?
我在这里看到了类似的问题,但没有一个问题是询问同样的内容。

2
这就是 fgets 的工作原理。它不会丢弃输入的其余部分,你的代码应该考虑到这一点。如果末尾没有 newline,那么这行就会变得更长(除非它是文件的最后一行)。虽然如果你错过了长度为1,下一个输入可能仅包含一个 newline - Weather Vane
你还没有说明你是想要丢弃过长的输入还是考虑它。前者:如果 fgets 字符串没有以 newline(当然是在 nul 之前)结尾,则使用 getchar 继续读取,直到出现 newlineEOF。后者:如果输入不包含最后的 newline,则使用分配的缓冲区和 realloc。这里有一个不同的问题,介绍了如何使用这种技术。http://stackoverflow.com/questions/28254245/c-reading-a-text-file-separated-by-spaces-with-unbounded-word-size/28255082#28255082 - Weather Vane
我认为你的问题最好陈述为如何处理所有的输入,而不是寻找将无法处理的输入丢弃的方法:这会导致GIGO。 - Weather Vane
@WeatherVane 正如您在提供的代码中所看到的,缓冲区是在堆栈上分配的,因此无法进行realloc操作。 - jacwah
@WeatherVane 有时候你不希望字符串超过一定长度。但在大多数情况下,读取整个输入是解决问题的正确方法。 - jacwah
显示剩余5条评论
2个回答

6
如何防止程序在下一次迭代中读取过长的输入内容?
代码需要实现以下两个功能:1)检测输入是否“过长”2)消耗多余的输入。
使用fgets()函数不会超出缓冲区。如果缓冲区已满,则缓冲区中最后一个字符为'\0'。因此,在读取之前将其设置为非'\0'。然后代码就知道整个缓冲区是否已满。然后检查前面的char是否为'\n'。如果不是行末,可能存在其他charstdin中。
char str[100];  // Insure buffer is at least size 2
for (;;) {
  str[sizeof str - 1] = `x`;
  if (fgets(str, size, stdin) == NULL) {
    // No more to read or IO error
    break;
  }
  int extra_data_found = 0;
  if (str[sizeof str - 1] == '\0' && str[sizeof str - 2] != '\n') {
    // Cope with potential extra data in `stdin`: read and toss
    int ch;
    while ((ch = fgetc(stdin)) != '\n' && ch != EOF) {
      extra_data_found = 1;
    }
  }
  // Use `str` as needed, noting if additional unsaved data found
  foo(str, extra_data_found);
}

注意:当文件出现错误时,fgets() 返回 NULL,并且 str 的内容是未定义的。
注意:代码可以使用 strlen(str) == sizeof str - 1 替代 str[sizeof str - 1] == '\0'。如果 fgets() 读取了空字符 '\0',这样做就会受到影响。
特殊情况: 1. 典型的 str 最多有 98 个 char,然后是 '\n''\0'。如果最后一个 str 是 99 个 char ,然后是 '\0',那么是否可以呢? 2. 如果 #1 可以,那么典型的 str 是否可以有 99 个 char,然后是 '\0'

3

如果输入过长,您需要在继续下一次循环之前从stdin中读取剩余的字符。

if (fgets(str, size, stdin) == NULL) {
    if (feof(stdin)) {
        return 0;
    else {
        perror("Could not read from stdin");
        exit(1);
    }
}
else if (strchr(str, '\n') == NULL) {
    int c;
    while((c = getc(stdin)) != '\n' && c != EOF);
    fprintf(stderr, "Input too long\n");
}

如果您使用像OS X或Linux这样的POSIX系统,已经存在getline函数,它可以从流中读取任意长度的以换行符终止的字符串。您也可以在网上找到许多免费/开源版本的此功能。


如果 (!feof(stdin)) ... "Could not read from stdin",这是可疑的。通常情况下,通过用户控制或重定向输入,stdin 将会提供一段时间的输入,然后 feof(stdin) 将为 true。这不是“无法从 stdin 读取”,更像是“没有要读取的内容了”。 - chux - Reinstate Monica
@chux fgets 可以在两种情况下返回 NULL:如果遇到文件结尾或发生错误。如果它返回了 NULL 且不是文件结尾,那么肯定是出现了错误,因此应该输出 "无法从 stdin 中读取"。代码还应该处理 feof(stdin) 为真的情况,我会编辑答案。 - jacwah
1
@jacwah同意fgets()有两种情况会返回NULL,但还有其他情况吗?请考虑病态案例:https://dev59.com/amAg5IYBdhLWcg3wnMC3(不需要因为这个改变答案) - chux - Reinstate Monica
@chux 好发现!我没有想到这个。 - jacwah

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