如何防止scanf导致C中的缓冲区溢出?


81

我使用以下代码:

while ( scanf("%s", buf) == 1 ){

防止缓冲区溢出以使其可以传递随机长度的字符串的最佳方法是什么?

我知道我可以通过调用例如来限制输入字符串:

while ( scanf("%20s", buf) == 1 ){

但是我更希望能够处理用户输入的任何内容。还是不能使用scanf安全地完成此操作,而我应该使用fgets?

Answers:


64

Kernighan和Pike在他们的《编程实践》(非常值得一读)一书中讨论了这个问题,他们通过使用snprintf()创建具有正确缓冲区大小的字符串以传递给scanf()函数族来解决此问题。有效:

int scanner(const char *data, char *buffer, size_t buflen)
{
    char format[32];
    if (buflen == 0)
        return 0;
    snprintf(format, sizeof(format), "%%%ds", (int)(buflen-1));
    return sscanf(data, format, buffer);
}

注意,这仍然将输入限制为作为“缓冲区”提供的大小。如果需要更多空间,则必须进行内存分配,或使用为您进行内存分配的非标准库函数。


请注意,的POSIX 2008(2013)版本scanf()系列的功能支持的格式修改m字符串输入(分配分配字符)( ,%s,)。%c %[它不带char *参数,而是带char **参数,并为其读取的值分配必要的空间:

char *buffer = 0;
if (sscanf(data, "%ms", &buffer) == 1)
{
    printf("String is: <<%s>>\n", buffer);
    free(buffer);
}

如果sscanf()函数不能满足所有转换规范,则%ms在函数返回之前,将释放为类似转换分配的所有内存。


@Sam:是的,应该buflen-1—谢谢。然后,您必须担心未签名的下溢(包装到相当大的数字),因此需要进行if测试。如果有人不注意传递0作为大小assert(),我很想用一个替换它,或者在开发assert()之前将if其替换为一个,以备不时之需。我没有仔细查看文档中的%0s含义sscanf()-该测试可能会更好if (buflen < 2)
Jonathan Leffler 2013年

因此,snprintf将一些数据写入字符串缓冲区,然后sscanf从创建的字符串中读取数据。scanf从stdin读取的内容究竟在哪里替换呢?
krb686 2015年

如果在结果字符串中使用单词“ format”,然后将“ format”作为第一个参数传递给snprintf它,但它并不是实际的format参数,这也很令人困惑。
krb686 2015年

@ krb686:编写此代码是为了使要扫描的数据在参数中data,因此sscanf()是适当的。如果要改为从标准输入中读取,请删除data参数并scanf()改为调用。至于format在调用中成为格式字符串的变量的名称选择sscanf(),您可以根据需要对它进行重命名,但是其名称并不准确。我不确定哪种选择是有意义的。会in_format更清楚吗?我不打算在此代码中进行更改。您可能会在自己的代码中使用这个想法。
乔纳森·莱夫勒

1
@mabraham:在macOS Sierra 10.12.5(至2017-06-06之前)中仍然如此-scanf()尚未将macOS上的文档记录为support %ms,尽管有用。
乔纳森·勒夫勒

30

如果使用的是gcc,则可以使用GNU扩展a说明符让scanf()为您分配内存以保存输入:

int main()
{
  char *str = NULL;

  scanf ("%as", &str);
  if (str) {
      printf("\"%s\"\n", str);
      free(str);
  }
  return 0;
}

编辑:正如Jonathan所指出的,您应该查阅scanf手册页,因为说明符可能不同(%m),并且在编译时可能需要启用某些定义。


8
使用glibc(GNU C库)比使用GNU C编译器要更重要。
乔纳森·勒夫勒

3
并请注意,POSIX 2008标准提供了m用于完成相同工作的修饰符。请参阅scanf()。您需要检查您使用的系统是否支持此修饰符。
乔纳森·莱夫勒

4
GNU(无论如何可以在Ubuntu 13.10上找到)都支持%ms。该符号%a是的同义词%f(在输出时,它请求十六进制浮点数据)。对于GNU手册页scanf()说:如果_程序编译它不可用gcc -std=c99或GCC -D_ISOC99_SOURCE(除非_GNU_SOURCE也指定了),在这种情况下,a被解释为说明符浮点数(见上文)._
Jonathan Leffler 2014年

8

在大多数情况下,fgetssscanf会结合使用。如果输入格式正确,另一件事就是编写自己的解析器。还要注意,您的第二个示例需要一些修改才能安全使用:

#define LENGTH          42
#define str(x)          # x
#define xstr(x)         str(x)

/* ... */ 
int nc = scanf("%"xstr(LENGTH)"[^\n]%*[^\n]", array); 

上面的内容将最多放弃输入流,但不包括换行符(\n)。您将需要添加一个getchar()以使用它。还要检查您是否达到了流媒体的结尾:

if (!feof(stdin)) { ...

就是这样。


2
您能否将feof代码放在更大的上下文中?我问,因为该功能经常被错误地使用。
罗兰·伊利格

1
array需要是char array[LENGTH+1];
jxh

4

直接使用scanf(3)及其变体带来许多问题。通常,用户和非交互用例是根据输入行定义的。很少见到这样的情况:如果找不到足够的对象,那么会有更多的行可以解决问题,但这是scanf的默认模式。(如果用户不知道在第一行输入数字,则第二行和第三行可能无济于事。)

至少如果您fgets(3)知道程序需要多少输入行,并且不会有任何缓冲区溢出...


1

限制输入的长度绝对容易。您可以使用循环来接受任意长的输入,一次读取一点,并根据需要为字符串重新分配空间...

但这是很多工作,因此大多数C程序员只是以任意长度截断输入。我想您已经知道这一点,但是使用fgets()不会允许您接受任意数量的文本-您仍然需要设置一个限制。


那么有人知道如何用scanf做到这一点吗?
goe

3
在循环中使用fgets可以允许您接受任意数量的文本-只需保留realloc()缓冲区即可。
bdonlan

1

创建一个为字符串分配所需内存的函数不需要太多工作。那是我前一段时间写的一个C函数,我总是用它来读取字符串。

它将返回读取的字符串或如果发生内存错误NULL。但是请注意,您必须free()字符串,并始终检查其返回值。

#define BUFFER 32

char *readString()
{
    char *str = malloc(sizeof(char) * BUFFER), *err;
    int pos;
    for(pos = 0; str != NULL && (str[pos] = getchar()) != '\n'; pos++)
    {
        if(pos % BUFFER == BUFFER - 1)
        {
            if((err = realloc(str, sizeof(char) * (BUFFER + pos + 1))) == NULL)
                free(str);
            str = err;
        }
    }
    if(str != NULL)
        str[pos] = '\0';
    return str;
}

sizeof (char)根据定义1。您在这里不需要它。
RastaJedi

通常,将指针分配/释放保持在同一级别是个好习惯,这意味着您的函数不应自行分配内存,因为调用者随后必须释放它。大多数标准库/ posix函数都遵循此原理,方法是返回静态字符串(如strerror(3))或期望传入预分配的字符串(如(strerror_r(3)-或scanf(3))...
Michael Beer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.