为什么获取功能如此危险以至于不应该使用它?

问题描述 投票:199回答:11

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

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

我记得这与堆栈保护和安全性有关,但我不确定为什么。

如何删除此警告以及为什么有关于使用gets()的警告?

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

c fgets buffer-overflow gets
11个回答
158
投票

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

而不是使用gets,你想使用具有签名的fgets

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

fgets,如果它读取整行,将把'\n'留在字符串中;你将不得不处理它。)

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


3
投票

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

fgets()允许您指定从标准输入缓冲区中取出的字符数,因此它们不会超出变量。


2
投票

C获得功能是危险的,并且是一个非常昂贵的错误。 Tony Hoare在他的演讲“Null References:The Billion Dollar Mistake”中特别提到了它:

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

整整一小时值得关注,但从30分钟开始他的评论观点,特别是在39分钟左右的批评。

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


137
投票

Why is gets() dangerous

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

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

C11标准ISO / IEC 9899:2011取消了gets()作为标准功能,即A Good Thing™(在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(),...)。

Alternatives to gets()

与fgets()

正如其他人所说的那样,gets()的规范替代方案是fgets(),将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()不会出现这个问题,因为它不知道你的缓冲区在哪里结束并且快速地踩到了结尾,对你精美的内存布局造成严重破坏,如果缓冲区被分配,通常会搞乱返回堆栈(Stack Overflow)如果缓冲区是动态分配的,则堆栈或践踏控制信息,或者如果缓冲区是静态分配的,则通过其他宝贵的全局(或模块)变量复制数据。这些都不是一个好主意 - 它们集中体现了“未定义的行为”这一短语。


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

§6.5.4.1gets_s函数

Synopsis

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

Runtime-constraints

s不应为空指针。 n既不等于零也不大于RSIZE_MAX。从n-1读取stdin字符时,应出现换行符,文件结束或读取错误.25)

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

Description

4 gets_s函数读取的最多值是n指向的流中stdin指定的字符数小于s指向的数组。在换行符(被丢弃)之后或文件结束之后,不会读取其他字符。丢弃的换行符不计入读取的字符数。在读入数组的最后一个字符后立即写入空字符。

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

Recommended practice

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

25)与gets_s不同,gets函数使得它成为一个输入行的运行时约束违例,以溢出缓冲区来存储它。与fgets不同,gets_s在输入线和成功调用gets_s之间保持一对一的关系。使用gets的程序期望这种关系。

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

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


getline() - POSIX

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


21
投票

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

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

gets(array1);

现在,首先你可以输入你想要多少个字符,gets不会关心它。其次,你放置它们的数组大小的字节(在这种情况下是array1)将覆盖它们在内存中找到的任何内容,因为gets会写它们。在前面的例子中,这意味着如果您输入"abcdefghijklmnopqrts"可能,不可预测,它也会覆盖array2或其他什么。

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


16
投票

你不应该使用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相同的保护,因为它可以防止缓冲区溢出,但它还会通知调用者发生了什么,并清除多余的字符,以便它们不会影响您的下一个输入操作。

随意使用它,如我所愿,我特此在“做你真该想做的”许可下发布:-)


11
投票

fgets

从stdin读取:

char string[512];

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

6
投票

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

这就是one reference给出的原因:

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


4
投票

我最近在USENET post to comp.lang.c读到,gets()正在从标准中删除。哇噢

你会很高兴地知道委员会刚刚投票(一致地,结果)从草案中删除了gets()。


4
投票

在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的调用者注意结果数组中是否存在换行符。考虑使用fgets(以及基于换行符的任何所需处理)而不是gets_s


3
投票

我想向任何C库维护人员发出诚挚的邀请,他们仍然在他们的库中包含gets“以防万一仍然依赖它”:请用相当于

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

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

© www.soinside.com 2019 - 2024. All rights reserved.