为什么 gets 函数如此危险,以至于不应该使用它?

Why is the gets function so dangerous that it should not be used?

提问人:Vinit Dhatrak 提问时间:11/8/2009 最后编辑:S.S. AnneVinit Dhatrak 更新时间:9/2/2022 访问量:234784

问:

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

(.text+0x34):警告:“gets”函数很危险,不应使用。

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

我怎样才能删除这个警告,为什么会有这样的关于使用的警告?gets()

如果如此危险,那我们为什么不能删除它呢?gets()

c fgets 缓冲区溢出 gets

评论

3赞 EsmaeelE 12/9/2017
gets()Buffer_overflow_attack
0赞 EsmaeelE 12/9/2017
更多explanation_Bufferoverflow_C gets()
7赞 William Pursell 12/16/2020
请注意,它存在与 相同的问题。scanf("%s", b)gets
2赞 Andrew 6/10/2022
为了衡量WG14(负责C标准的ISO工作组)对此的重视程度,到目前为止,它是C标准中唯一正式删除的功能。WG14 有一个永不破坏现有代码的政策(即使已经从根本上破坏了)——他们打破了这个政策来摆脱!gets()

答:

222赞 Thomas Owens 11/8/2009 #1

为了安全使用,您必须确切地知道您将读取多少个字符,以便您可以使缓冲区足够大。只有当您确切地知道您将要读取哪些数据时,您才会知道这一点。gets

您希望使用 fgets,而不是使用 ,它具有签名gets

char* fgets(char *string, int length, FILE * stream);

(fgets,如果它读取了一整行,则会在字符串中留下 ;你必须处理这个问题。'\n'

gets直到 1999 年 ISO C 标准,它仍然是该语言的官方部分,但在 2011 年标准中被正式删除。大多数 C 实现仍然支持它,但至少 gcc 会对使用它的任何代码发出警告。

评论

97赞 fuz 1/5/2015
实际上,发出警告的不是 gcc,而是包含编译指示或属性的 glibc,它会导致编译器在使用时发出警告。gets()
7赞 Ruslan 4/21/2020
@fuz实际上,发出警告的甚至不仅仅是编译器:OP 中引用的警告是由链接器打印的!
12赞 Gerd Klima 11/8/2009 #2

在不中断 API 的情况下,无法删除 API 函数。如果愿意,许多应用程序将不再编译或运行。

这就是一个参考文献给出的原因:

读取溢出的行 s 指向的数组结果 未定义的行为。fgets() 的使用 是推荐的。

27赞 Jack 11/8/2009 #3

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

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

gets(array1);

现在,首先,您可以输入所需的字符数,不会在乎。其次,超过放置它们的数组大小的字节(在本例中)将覆盖它们在内存中找到的任何内容,因为会写入它们。在前面的示例中,这意味着如果您输入 maybe,不可预测,它也会覆盖其他内容。getsarray1gets"abcdefghijklmnopqrts"array2

该函数不安全,因为它假定输入一致。永远不要使用它!

评论

4赞 legends2k 9/30/2013
完全不可用的是它没有它所接受的数组长度/计数参数;如果它在那里,它只是另一个普通的 C 标准函数。gets
1赞 supercat 3/29/2015
@legends2k:我很好奇预期的用途是什么,为什么没有标准的 fgets 变体对于不需要换行符作为输入一部分的用例那么方便?gets
3赞 legends2k 3/29/2015
顾名思义,@supercat被设计为从 中获取字符串,但是没有 size 参数的理由可能来自 C 的精神:信任程序员。此函数在 C11 中删除,给定的替换gets_s采用输入缓冲区的大小。不过我对这部分一无所知。getsstdinfgets
0赞 supercat 3/30/2015
@legends2k:我能看到的唯一可以原谅的情况是,如果一个人使用硬件线路缓冲的 I/O 系统,该系统在物理上无法提交超过一定长度的线路,并且程序的预期寿命短于硬件的寿命。在这种情况下,如果硬件无法提交超过 127 字节长的行,那么将 128 字节的缓冲区提交到一个 128 字节的缓冲区中可能是合理的,尽管我认为在期望较小的输入时能够指定更短的缓冲区的优势将远远超过成本的合理性。getsgets
0赞 supercat 3/30/2015
@legends2k:实际上,最理想的情况是让“字符串指针”标识一个字节,该字节将在几种不同的字符串/缓冲区/缓冲区信息格式中进行选择,其中一个前缀字节值表示包含前缀字节 [加填充]的结构,加上缓冲区大小、使用的大小和实际文本的地址。这种模式将使代码能够传递另一个字符串的任意子字符串(而不仅仅是尾部),而不必复制任何内容,并且允许像 和 这样的方法安全地接受尽可能多的内容。getsstrcat
6赞 pmg 11/8/2009 #4

我最近在 comp.lang.c 的 USENET 帖子中读到,它正在从标准中删除。呜呼gets()

你会很高兴地知道 委员会刚刚投票(一致通过,如 事实证明)从 草案也是如此。

评论

3赞 Jonathan Leffler 11/8/2009
它被从标准中删除是非常好的。但是,由于向后兼容性,大多数实现至少在未来 20 年内将其作为“现在的非标准扩展”提供。
1赞 pmg 11/8/2009
是的,没错,但是当你用gets()编译时,不会通过。(我刚刚编造了参数)gcc -std=c2012 -pedantic ...-std
16赞 Thiago Silveira 11/30/2010 #5

fgets

要从 stdin 中读取:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */
237赞 Jonathan Leffler 11/30/2010 #6

为什么很危险gets()

第一个互联网蠕虫(Morris Internet 蠕虫)在大约 30 年前(1988-11-02)逃脱,它使用缓冲区溢出作为其从一个系统传播到另一个系统的方法之一。基本问题是该函数不知道缓冲区有多大,因此它继续读取,直到找到换行符或遇到 EOF,并且可能会溢出给定缓冲区的边界。gets()

你应该忘记你曾经听说过它的存在。gets()

C11 标准 ISO/IEC 9899:2011 作为标准功能被取消,这是一件好事™(它在 ISO/IEC 9899:1999/Cor.3:2007 — C99 的技术勘误 3 中被正式标记为“过时”和“弃用”,然后在 C11 中删除)。可悲的是,由于向后兼容性的原因,它将在库中保留多年(即“几十年”)。如果由我来决定,那么gets()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() 指定为文件流。gets()stdin

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()


还有TR 24731-1(C标准委员会的技术报告),它为各种功能提供了更安全的替代方案,包括:gets()

§6.5.4.1 函数gets_s

###Synopsis

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

运行时约束

s不应为 null 指针。 既不得等于零,也不得大于RSIZE_MAX。在从 读取字符时,应发生换行符、文件末尾或读取错误。25)nn-1stdin

3 如果存在运行时约束冲突,则设置为 null 字符,并且读取和丢弃字符,直到读取换行符,或者发生文件结束或读取错误。s[0]stdin

描述

4 该函数最多从 所指向的流中读取比 指定的字符数少 1 个字符数到 所指向的数组中。在换行符(被丢弃)或文件末尾之后不会读取其他字符。丢弃的换行符不计入读取的字符数。null 字符在读入数组的最后一个字符之后立即写入。gets_snstdins

5 如果遇到文件末尾并且没有字符读入数组,或者在操作过程中发生读取错误,则设置为空字符,并且 的其他元素采用未指定的值。s[0]s

推荐做法

6 该函数允许正确编写的程序安全地处理输入行,输入行太长而无法存储在结果数组中。通常,这要求调用方注意结果数组中是否存在换行符。请考虑使用(以及基于换行符的任何所需处理)而不是 .fgetsfgetsfgetsgets_s

25) 与 不同,该函数使一行输入溢出缓冲区以存储它成为运行时约束冲突。与 不同,在输入行和成功调用 之间保持一对一的关系。使用这种关系的程序。gets_sgetsfgetsgets_sgets_sgets

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

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


getline()— POSIX的

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

评论

9赞 caf 11/30/2010
还值得指出的是,您的版本会将过长行的尾部保留在输入缓冲区中,供下一个输入函数读取。在许多情况下,您需要读取和丢弃这些字符。fgets()fgets_wrapper()
9赞 supercat 3/28/2015
我想知道为什么他们没有添加一个 fgets() 替代方案,允许人们使用其功能而无需进行愚蠢的 strlen 调用。例如,返回读入字符串的字节数的 fgets 变体将使代码能够轻松查看读取的最后一个字节是否为换行符。如果为缓冲区传递 null 指针的行为定义为“读取并丢弃最多 n-1 个字节,直到下一个换行符”,这将允许代码轻松丢弃超长行的尾部。
3赞 Jonathan Leffler 3/28/2015
@supercat:是的,我同意——很遗憾。最接近的方法可能是 POSIX getline() 及其相对 ,它们确实返回命令读取的“行”的长度,并根据需要分配空间以能够存储整行。即使这样也会导致问题,如果您最终得到一个大小为数 GB 的单行 JSON 文件;你能买得起所有这些内存吗?(当我们使用它时,我们是否可以拥有 和 变体,在末尾返回指向空字节的指针?等等)getdelim()strcpy()strcat()
4赞 Jonathan Leffler 3/28/2015
@supercat:另一个问题是,如果文件包含一个空字节,你就无法知道在空字节之后有多少数据,直到行尾(或EOF)。 最多只能报告数据中的空字节;在那之后,这是猜测,因此几乎可以肯定是错误的。fgets()strlen()
7赞 candied_orange 1/24/2016
“忘了你听说过它的存在。”当我这样做时,我再次遇到它并回到这里。您是否正在入侵 stackoverflow 以获得赞成票?gets()
18赞 paxdiablo 11/30/2010 #7

不应使用,因为它无法阻止缓冲区溢出。如果用户输入的数据超出了缓冲区的容量,则很可能最终导致损坏或更糟。gets

事实上,ISO 实际上已经采取了从 C 标准中删除的步骤(从 C11 开始,尽管它在 C99 中被弃用),考虑到它们对向后兼容性的评价有多高,这应该表明该功能有多糟糕。gets

正确的做法是将函数与文件句柄一起使用,因为您可以限制从用户读取的字符。fgetsstdin

但这也有其问题,例如:

  • 用户输入的额外字符将在下次选取。
  • 没有用户输入过多数据的快速通知。

为此,几乎每个 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

随意使用它,我特此在“做你该死的好想做的事”许可下发布它:-)

评论

0赞 Jonathan Leffler 6/21/2015
实际上,原始的 C99 标准并没有在定义它的第 7.19.7.7 节中明确弃用,也没有在第 7.26.9 节“未来库方向”和 .甚至没有脚注说明它是危险的。(话虽如此,我在 Yu Hao回答中看到“它在 ISO/IEC 9899:1999/Cor.3:2007(E))中已弃用”。但 C11 确实将其从标准中删除了——而且不是在时间之前!gets()<stdio.h>
0赞 chux - Reinstate Monica 8/30/2016
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)隐藏 的 到 转换。 会捕获 的奇怪值。size_tintszsz > INT_MAX || sz < 2sz
0赞 chux - Reinstate Monica 8/30/2016
if (buff[strlen(buff)-1] != '\n') {是一个黑客漏洞,因为邪恶用户输入的第一个字符可能是嵌入的空字符渲染 UB。 如果用户输入空字符,则有问题。buff[strlen(buff)-1]while (((ch = getchar())...
5赞 Yu Hao 10/6/2013 #8

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

除此之外,C11 还引入了一种新的安全替代方案:fgets()gets_s()

C11 K.3.5.4.1 函数gets_s

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

但是,在“推荐做法”部分中,仍然是首选。fgets()

该函数也允许正确编写的程序安全地处理输入行 long 存储在 result 数组中。一般来说,这需要付费的呼叫者 注意结果数组中是否存在换行符。考虑 使用(以及基于换行符的任何所需处理)而不是 .fgetsfgetsfgetsgets_s

评论

0赞 mrKirushko 5/14/2021
如果他们删除 fgets(),总会有像 scanf(“%s”, arr) 或 getline(&arr, 100500, stdin) 这样的选项。这当然是一件令人讨厌的事情,因为当你想写一些糟糕的代码时,你通常也希望尽可能快地完成它,并且使用最少的脑力。我希望实现将只停留在警告上。
0赞 Ben Voigt 10/24/2023
@mrKirushko:你的选择和.scanf("%s", arr)gets()
1赞 Steve Summit 4/1/2016 #9

我想向任何仍在他们的库中包含“以防万一仍然依赖它”的 C 库维护者发出诚挚的邀请: 请将您的实现替换为等效的gets

char *gets(char *str)
{
    strcpy(str, "Never use gets!");
    return str;
}

这将有助于确保没有人仍然依赖它。谢谢。

评论

0赞 mrKirushko 5/14/2021
即使他们删除了 fgets(),也总是有像 scanf(“%s”, arr) 或 getline(&arr, 100500, stdin) 这样的选项可用。当然,这仍然是一个麻烦,因为当你想写一些糟糕的代码时,你通常也希望尽可能快地完成它,并且使用最少的脑力。我希望实现将只停留在警告上。
0赞 FFmpegEnthusiast 8/5/2023
@mrKirushko 他们为什么要删除?这是安全的,不像fgets()gets()
3赞 user3717661 5/1/2016 #10

C gets函数是危险的,并且是一个非常昂贵的错误。Tony Hoare 在他的演讲“Null References: The Billion Dollar Mistake”中特别提到了这一点:

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

整整一个小时都值得一看,但对于他的评论,从 30 分钟开始,具体在 39 分钟左右受到批评。

希望这能激起你对整个演讲的兴趣,它引起了人们的注意,即我们如何需要更正式的语言正确性证明,以及语言设计者应该如何为他们的语言中的错误负责,而不是程序员。这似乎是不良语言设计者以“程序员自由”为幌子将责任推给程序员的全部可疑原因。

5赞 Aradhana Mohanty 8/22/2017 #11

gets()很危险,因为用户可能会因在提示中键入过多内容而使程序崩溃。它无法检测到可用内存的末尾,因此,如果分配的内存量太小,则可能会导致段故障和崩溃。有时,用户似乎不太可能在提示中输入 1000 个字母来输入一个人的名字,但作为程序员,我们需要使我们的程序防弹。(如果用户可能因发送过多数据而使系统程序崩溃,也可能存在安全风险)。

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

评论

0赞 Tanz87 10/14/2017
请注意,真正的危险不在于能够使程序崩溃,而在于能够使它运行任意代码。(通常,利用未定义的行为
0赞 a573263 2/28/2022 #12

简而言之,(可能)是危险的,因为用户输入的内容可能大于变量有足够的空间来存储的内容。第一个答案是关于它以及为什么它更安全。gets()fgets()

评论

2赞 jpa 2/28/2022
这个答案只是不必要地重复别人已经说过的话。
1赞 a573263 3/1/2022
@jpa真的。我只是想用尽可能少的字说出来
1赞 Gabriel Staples 3/6/2022 #13

附加信息:

在Linux Ubuntu上,你会看到(强调后加):man 3 gets

DESCRIPTION
       Never use this function.

而且,从这里的 cppreference.com wiki (https://en.cppreference.com/w/c/io/gets) 你会看到:Notes Never use gets().

笔记

该函数不执行边界检查,因此此函数极易受到缓冲区溢出攻击。它不能安全地使用(除非程序在限制可以出现的内容的环境中运行)。因此,该功能在 C99 标准的第三份勘误中已被弃用,并在 C11 标准中完全删除。 并且是推荐的替代品。gets()stdinfgets()gets_s()

永远不要使用 gets()。

如您所见,该函数已在 C11 或更高版本中弃用并完全删除。

请改用 fgets()gets_s()。

这是我的演示用法,带有完整的错误检查:fgets()

来自 read_stdin_fgets_basic_input_from_user.c

#include <errno.h>   // `errno`
#include <stdio.h>   // `printf()`, `fgets()`
#include <stdlib.h>  // `exit()`
#include <string.h>  // `strerror()`

// int main(int argc, char *argv[])  // alternative prototype
int main()
{
    char buf[10];

    // NEVER USE `gets()`! USE `fgets()` BELOW INSTEAD!

    // USE THIS!: `fgets()`: "file get string", which reads until either EOF is
    // reached, OR a newline (`\n`) is found, keeping the newline char in
    // `buf`.
    // For `feof()` and `ferror()`, see:
    // 1. https://en.cppreference.com/w/c/io/feof
    // 1. https://en.cppreference.com/w/c/io/ferror
    printf("Enter up to %zu chars: ", sizeof(buf) - 1); // - 1 to save room
                                                        // for null terminator
    char* retval = fgets(buf, sizeof(buf), stdin);
    if (feof(stdin))
    {
        // Check for `EOF`, which means "End of File was reached".
        // - This doesn't really make sense on `stdin` I think, but it is a good
        //   check to have when reading from a regular file with `fgets
        //   ()`. Keep it here regardless, just in case.
        printf("EOF (End of File) reached.\n");
    }
    if (ferror(stdin))
    {
        printf("Error indicator set. IO error when reading from file "
               "`stdin`.\n");
    }
    if (retval == NULL)
    {
        printf("ERROR in %s(): fgets() failed; errno = %i: %s\n",
            __func__, errno, strerror(errno));

        exit(EXIT_FAILURE);
    }

    size_t num_chars_written = strlen(buf) + 1; // + 1 for null terminator
    if (num_chars_written >= sizeof(buf))
    {
        printf("Warning: user input may have been truncated! All %zu chars "
               "were written into buffer.\n", num_chars_written);
    }
    printf("You entered \"%s\".\n", buf);


    return 0;
}

示例运行和输出:

eRCaGuy_hello_world/c$ gcc -Wall -Wextra -Werror -O3 -std=c17 read_stdin_fgets_basic_input_from_user.c -o bin/a && bin/a
Enter up to 9 chars: hello world!
Warning: user input may have been truncated! All 10 chars were written into buffer.
You entered "hello wor".

eRCaGuy_hello_world/c$ gcc -Wall -Wextra -Werror -O3 -std=c17 read_stdin_fgets_basic_input_from_user.c -o bin/a && bin/a
Enter up to 9 chars: hey
You entered "hey
".