为什么gets函数很危险,不应该使用?

310

当我使用GCC编译使用gets()函数的C代码时,会收到以下警告:

(.text+0x34): 警告:`gets'函数是危险的,不应该使用。

我记得这与堆栈保护和安全有关,但我不确定具体原因。

如何消除此警告?为什么使用gets()会产生这样的警告?

如果gets()如此危险,为什么我们不能将其删除?


3
gets()缓冲区溢出攻击gets()函数是一种不安全的输入方法,因为它无法检测用户输入的大小。攻击者可以利用这个漏洞来向程序中的缓冲区写入超出其分配大小的数据,导致缓冲区溢出攻击。攻击者可以利用这种漏洞执行恶意代码或者修改程序的逻辑。为了避免这种攻击,建议使用更加安全的输入方法,如fgets() - EsmaeelE
7
请注意,scanf("%s", b) 存在与 gets 相同的问题。 - William Pursell
2
作为衡量WG14(负责C标准的ISO工作组)对此问题的严肃程度的一项措施,到目前为止,这是唯一一个正式从C标准中删除的功能。WG14有一个“永远不破坏现有代码”的政策(即使已经根本无法使用),他们打破了这个政策来摆脱gets() - Andrew
13个回答

237

为什么gets()是危险的

第一次互联网蠕虫(莫里斯互联网蠕虫)于30年前逃逸出来(1988-11-02),它使用gets()和缓冲区溢出作为其从系统到系统传播的方法之一。基本问题在于该函数不知道缓冲区的大小,因此它会继续读取,直到找到换行符或遇到EOF,并可能超出所给定的缓冲区边界。

你应该忘记你曾听说过gets()

C11标准ISO/IEC 9899:2011将gets()作为标准函数删除,这是一件好事™(它在ISO/IEC 9899:1999/Cor.3:2007 - C99的技术勘误3中被正式标记为“过时”和“弃用”,然后在C11中被删除)。不幸的是,由于向后兼容性的原因,它将在许多年(即“几十年”)的库中保留。如果由我决定,gets()的实现将变成:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}

考虑到你的代码迟早会崩溃,最好尽早解决问题。我建议添加一个错误信息:

fputs("obsolete and dangerous function gets() called\n", stderr);

现代Linux编译系统会在链接gets()以及其他存在安全问题的函数(mktemp()等)时生成警告。

gets()的替代方法

fgets()

正如其他人所说,可以使用fgets()并将stdin作为文件流来替代gets()

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}

还没有人提到的是,gets()函数不包括换行符,但fgets()函数会包括。因此,您可能需要使用一个包装器来删除fgets()函数中的换行符:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        return buffer;
    }
    return 0;
}

或者,更好的是:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        buffer[strcspn(buffer, "\n")] = '\0';
        return buffer;
    }
    return 0;
}

另外,正如caf在评论中指出的那样和paxdiablo在他们的答案中展示的那样,使用fgets()时可能会有数据留在一行上。我的包装代码将该数据留待下次读取;如果您愿意,可以轻松修改它以吞噬剩余的数据行:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }

残留的问题是如何报告三种不同的结果状态——EOF或错误、读取的行未被截断和读取的部分行被截断且数据丢失。
这个问题在使用gets()时不会出现,因为它不知道你的缓冲区在哪里结束,随意践踏超过尾部,在美好地布置内存布局上制造破坏,通常会混乱返回堆栈(Stack Overflow)如果缓冲区在堆栈上分配,或者践踏控制信息如果缓冲区是动态分配的,或者复制数据到其他珍贵的全局(或模块)变量如果缓冲区是静态分配的。这些都不是一个好主意——它们是“未定义行为”的典型。

还有TR 24731-1(C标准委员会的技术报告),提供了一些更安全的函数替代方案,包括gets()

§6.5.4.1 gets_s函数

###概要

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

运行时约束

s 不得为 null 指针。n 既不等于零也不大于 RSIZE_MAX。从 stdin 读取 n-1 个字符时,应出现新行字符、文件结尾或读取错误。25)

如果存在运行时约束违规情况,则将 s[0] 设置为 null 字符,并从 stdin 中读取并丢弃字符,直到读取到一个新行字符或文件结尾或发生读取错误为止。

描述

gets_s 函数最多从指向 stdin 的流中读取 n 指定的字符数减一的数量,读入指向 s 的数组。在新行字符(被丢弃)或文件结尾之后,不会读取其他字符。被丢弃的新行字符不计入读取的字符数。null 字符将立即写入到数组中最后一个读取的字符之后。

如果在操作期间遇到文件结尾并且没有将任何字符读入数组中,或者发生读取错误,则将 s[0] 设置为 null 字符,并使 s 的其他元素采用未指定的值。

建议实践

6 fgets函数允许适当编写的程序安全地处理输入行,这些行太长而无法存储在结果数组中。一般来说,调用fgets的调用者需要注意结果数组中是否存在换行符。考虑使用fgets(以及基于换行符的任何必要处理)来代替gets_s

25)gets不同,gets_s函数将使输入行溢出缓冲区而无法存储成为运行时约束违规。与fgets不同,gets_s保持输入行与成功调用gets_s之间的一对一关系。使用gets的程序期望这种关系。

Microsoft Visual Studio编译器实现了TR 24731-1标准的近似版本,但Microsoft实现的签名与TR中的签名存在差异。

C11标准ISO/IEC 9899-2011将TR24731作为库的可选部分包含在附录K中。不幸的是,在类Unix系统上很少实现它。


getline() — POSIX

POSIX 2008提供了一个安全的替代gets()的方法,称为getline()。它动态分配行空间,因此您最终需要释放它。它消除了对行长度的限制。它还返回读取的数据长度,或者-1(而不是EOF!),这意味着输入中的空字节可以可靠地处理。还有一种“选择自己的单字符定界符”的变体,称为getdelim();如果您正在处理来自find -print0的输出,其中文件名的末尾用ASCII NUL '\0'字符标记,那么这可能很有用。


9
值得一提的是,fgets() 和您的 fgets_wrapper() 版本都会将超长行的尾部留在输入缓冲区中,以供下一个输入函数读取。在许多情况下,您需要读取和丢弃这些字符。 - caf
9
我想知道为什么他们没有添加一个fgets()的替代方法,使人们在不进行愚蠢的strlen调用的情况下使用其功能。例如,返回读入字符串的字节数的fgets变体将使代码轻松查看最后一个字节是否是换行符。如果将缓冲区参数传递为空指针的行为定义为“读取并丢弃多达n-1个字节,直到下一个换行符”,这将允许代码轻松地丢弃超长行的尾部。 - supercat
3
@supercat:是的,我同意-这真是遗憾。最接近的方法可能是POSIX getline()及其相关的getdelim(),它们返回由命令读取的'行'的长度,并分配所需的空间以便能够存储整个行。即使如此,在你有一个单行JSON文件大小为几个GB时也会导致问题;你能承担所有这些内存吗?(顺便问一下,我们能否有返回指向末尾的空字节的strcpy()strcat()变体等等?) - Jonathan Leffler
4
@supercat: fgets()的另一个问题是,如果文件包含空字节(null byte),你无法确定在空字节后到行末(或EOF)还有多少数据。strlen()只能报告数据中空字节之前的长度;在空字节之后,就只能靠猜测了,因此几乎肯定会出错。 - Jonathan Leffler
7
请忘记你曾经听说过 gets() 存在的事情。当我这样做时,我又会遇到它并回到这里。你是在黑客StackOverflow以获取赞吗? - candied_orange
显示剩余11条评论

222
为了安全地使用gets,您必须确切知道要读取多少个字符,以便可以使缓冲区足够大。只有当您确切知道要读取的数据时,才会知道这一点。
相比使用gets,您应该使用fgets,它具有以下签名:
char* fgets(char *string, int length, FILE * stream);

(fgets函数如果读取整行,会将'\n'留在字符串中,你需要处理它。) gets函数在1999年的ISO C标准中仍是官方部分,但在2011标准中已被正式移除。大多数C实现仍然支持它,但至少gcc会对使用它的任何代码发出警告。

97
实际上发出警告的不是gcc,而是glibc。gets()函数在glibc中包含一个pragma或attribute,导致编译器在使用时发出警告。 - fuz
7
实际上,不仅编译器发出警告:OP中引用的警告是由链接器打印的! - Ruslan

27

因为 gets 在从 stdin 获取字节并将它们放在某个地方时不会执行任何检查。一个简单的例子:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);

首先,你可以输入任意数量的字符,gets 不会限制。其次,如果你输入的字节数超过了数组(在本例中是 array1)的大小,它们就会覆盖内存中的任何东西,因为 gets 将写入它们。在前面的例子中,这意味着如果你输入 "abcdefghijklmnopqrts",可能会不可预测地覆盖 array2 或其他内容。

这个函数是不安全的,因为它假设输入始终是一致的。永远不要使用它!


4
gets 完全无法使用的原因是它没有接受数组长度/计数参数;如果有这个参数,它就只是另一个普通的 C 标准函数。 - legends2k
1
@legends2k:我很好奇gets的预期用途是什么,为什么没有制作标准的fgets变体以方便那些不希望换行符成为输入的一部分的用例? - supercat
3
@supercat gets函数的名称暗示它是用来从stdin中获取字符串的,然而对于没有包含_size_参数的理由可能是出于_C语言的精神_: 信任程序员。这个函数在_C11_中被移除,并且提供了一个替代方案gets_s,它需要输入缓冲区的大小。至于fgets部分我不清楚。 - legends2k
@legends2k:我唯一能想到的情况是,如果使用的是硬件行缓冲I/O系统,并且该系统在物理上无法提交超过某个长度的行,而程序的预期寿命短于硬件的寿命,则可能可以容忍使用gets。在这种情况下,如果硬件无法提交超过127字节长的行,则将其gets到128字节缓冲区中可能是合理的,尽管我认为当期望较小的输入时能够指定较短的缓冲区的优点足以弥补成本。 - supercat
这样的调整大小方法甚至可以在堆分配的字符串上执行realloc,使得一个只接受字符串参数的gets可以接受本地堆栈上的小数组的直接引用,或者是一个“自动调整大小的堆字符串”描述符的引用(允许根据需要分配堆存储)。 - supercat
显示剩余4条评论

18

你不应该使用 gets 函数,因为它没有办法停止缓冲区溢出。如果用户输入的数据超出了你的缓冲区大小,你很可能会遇到破坏或更糟糕的情况。

事实上,ISO 实际上已经从 C 标准中删除了 gets 函数(自 C11 开始,尽管它在 C99 中被弃用),这应该说明这个函数有多么糟糕,考虑到他们非常重视向后兼容性。

正确的做法是使用 fgets 函数和 stdin 文件句柄,因为你可以限制从用户读取的字符数。

但是这也有其问题,例如:

  • 用户输入的额外字符将在下一次读取时被选中。
  • 没有快速通知用户输入太多数据的功能。

为此,几乎每个 C 程序员在其职业生涯的某个时候都会编写一个更有用的 fgets 包装器。以下是我的:

#include <stdio.h>
#include <string.h>

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[strlen(buff)-1] = '\0';
    return OK;
}

带有一些测试代码:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        printf ("No input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long\n");
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

它提供与fgets相同的保护措施,以防止缓冲区溢出,但它还会通知调用者发生了什么,并清除多余的字符,以使它们不影响您的下一个输入操作。

请随意使用它,我在此释放它使用“你他妈想干啥就干啥”的许可证:-)


实际上,原始的C99标准在其定义gets()的7.19.7.7节或<stdio.h>子节中的7.26.9未明确弃用它。甚至没有关于它危险性的脚注。(话虽如此,我在Yu Hao答案中看到了"它在ISO/IEC 9899:1999/Cor.3:2007(E)中被弃用")。但是,C11从标准中删除了它——这是时候了! - Jonathan Leffler
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL) 隐藏了 size_tint 的转换。 sz > INT_MAX || sz < 2 可以捕捉到 sz 的异常值。 - chux - Reinstate Monica
如果 (buff[strlen(buff)-1] != '\n') { 是黑客的攻击方式,因为恶意用户输入的第一个字符可能是嵌入的空字符,导致 buff[strlen(buff)-1] UB。 如果用户输入空字符,则 while (((ch = getchar())... 会出现问题。 - chux - Reinstate Monica

16

fgets函数。

从标准输入读取:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */

12

如果您删除API函数,则会破坏API。如果这样做,许多应用程序将无法编译或运行。

这就是为什么一个参考资料提供了以下内容:

读取超出由s指向的数组的行会导致未定义的行为。建议使用fgets()。


6

我最近在一篇发给comp.lang.c的USENET帖子中读到,gets()将从标准中删除。太好了!

你会很高兴地知道,委员会刚刚投票(结果是全票通过),决定将gets()从草案中删除。


3
很棒它被从标准中移除了。但是,由于向后兼容性,大多数实现将在未来至少20年中将其提供为“非标准扩展”。 - Jonathan Leffler
1
是的,没错,但是当你使用 gcc -std=c2012 -pedantic ... 进行编译时,gets() 将无法通过。(我只是举例说明了 -std 参数) - pmg

5

在C11(ISO/IEC 9899:201x)中,gets()已被删除。(它在ISO/IEC 9899:1999/Cor.3:2007(E)中被弃用)

除了fgets(),C11还引入了一个新的安全替代品gets_s()

C11 K.3.5.4.1 The gets_s function

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

然而,在推荐实践部分,仍然更喜欢使用fgets()

fgets函数允许编写正确的程序安全地处理输入行,这些行太长而无法存储在结果数组中。一般来说,这要求调用者注意结果数组中是否存在换行符。考虑使用fgets(以及基于换行符的任何所需处理)代替gets_s


如果他们移除了fgets()函数,总还有其他选项,比如scanf("%s", arr)或者getline(&arr, 100500, stdin)。当然,这是一件麻烦的事情,因为当你想写一些糟糕的代码时,通常也希望尽可能快地完成,并且使用最少的脑力。我希望实现只会停留在警告上。 - mrKirushko
@mrKirushko:你的scanf("%s", arr)选项和gets()一样有问题。 - undefined

5

gets()是危险的,因为用户可能通过在提示中键入过多内容来使程序崩溃。它无法检测可用内存的结束,因此如果您为特定目的分配了太小的内存量,则可能会导致段错误并崩溃。有时,似乎很不可能让用户在为人名而设计的提示中键入1000个字母,但作为程序员,我们需要使我们的程序防弹。(如果用户可以通过发送过多数据来使系统程序崩溃,这也可能是安全风险)。

fgets()允许您指定从标准输入缓冲区中取出多少个字符,以便它们不会超出变量范围。


请注意,真正的危险不在于能够“崩溃”您的程序,而在于能够使其运行“任意代码”。(通常是利用“未定义行为”来实现的。) - Tanz87

3
C语言的gets函数是危险的,也是一个非常昂贵的错误。托尼·霍尔(Tony Hoare)在他的演讲"空指针:亿万美元的错误"中特别提到了它:http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare。整个小时值得一看,但他具体评论的gets问题可以从30分钟处开始观看,39分钟左右有相关的点评。
希望这为您打开了整个演讲的胃口,演讲引起了人们对语言需要更多形式上的正确性证明的关注,以及语言设计者应该为其语言中的错误负责,而不是程序员。这似乎是糟糕的语言设计者把责任推给程序员的可疑理由,冠名为“程序员自由”。

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