Por que a função gets é tão perigosa que não deve ser usada?

229

Quando tento compilar o código C que usa a gets()função com o GCC, recebo este aviso:

(.text + 0x34): aviso: a função `gets 'é perigosa e não deve ser usada.

Lembro que isso tem algo a ver com a proteção e a segurança da pilha, mas não sei exatamente por que.

Como posso remover este aviso e por que existe um aviso sobre o uso gets()?

Se gets()é tão perigoso, por que não podemos removê-lo?

Vinit Dhatrak
fonte

Respostas:

179

Para usar getscom segurança, você precisa saber exatamente quantos caracteres você estará lendo, para poder aumentar seu buffer o suficiente. Você só saberá que, se souber exatamente quais dados estará lendo.

Em vez de usar gets, você deseja usar fgets, que tem a assinatura

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

( fgets, se ler uma linha inteira, deixará '\n'a string; você terá que lidar com isso.)

Ele permaneceu uma parte oficial do idioma até o padrão ISO C de 1999, mas foi oficialmente removido pelo padrão de 2011. A maioria das implementações de C ainda o suporta, mas pelo menos o gcc emite um aviso para qualquer código que o utilize.

Thomas Owens
fonte
79
Na verdade, não é o gcc que avisa, é o glibc que contém um pragma ou atributo gets()que faz com que o compilador emita um aviso quando usado.
fuz 5/01/2015
@ fuz, na verdade, não é apenas o compilador que alerta: o aviso citado no OP foi impresso pelo vinculador!
Ruslan
163

Por que é gets()perigoso

O primeiro worm da Internet (o Morris Internet Worm ) escapou cerca de 30 anos atrás (02/11/1988) e usou gets()um estouro de buffer como um de seus métodos de propagação de um sistema para outro. O problema básico é que a função não sabe qual o tamanho do buffer, portanto continua lendo até encontrar uma nova linha ou encontrar um EOF e pode exceder os limites do buffer que recebeu.

Você deve esquecer que já ouviu falar que gets()existia.

A norma C11 ISO / IEC 9899: 2011 foi eliminada gets()como uma função padrão, que é A Good Thing ™ (foi formalmente marcada como 'obsoleta' e 'obsoleta' na ISO / IEC 9899: 1999 / Cor.3: 2007 - Corrigenda técnica 3 para C99 e depois removido em C11). Infelizmente, ele permanecerá nas bibliotecas por muitos anos (significando 'décadas') por razões de compatibilidade com versões anteriores. Se dependesse de mim, a implementação de gets()se tornaria:

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

Como seu código falhará de qualquer maneira, mais cedo ou mais tarde, é melhor resolver o problema mais cedo ou mais tarde. Eu estaria preparado para adicionar uma mensagem de erro:

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

As versões modernas do sistema de compilação do Linux geram avisos se você vincular gets()- e também para algumas outras funções que também apresentam problemas de segurança ( mktemp(),…).

Alternativas para gets()

fgets ()

Como todo mundo disse, a alternativa canônica gets()é fgets()especificar stdincomo o fluxo de arquivos.

char buffer[BUFSIZ];

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

O que ninguém mais mencionou ainda é que gets()não inclui a nova linha, mas fgets()inclui. Portanto, pode ser necessário usar um wrapper fgets()que exclua a nova linha:

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;
}

Ou melhor:

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;
}

Além disso, como caf aponta em um comentário e paxdiablo mostra em sua resposta, fgets()você pode ter dados restantes em uma linha. Meu código de wrapper deixa esses dados para serem lidos na próxima vez; você pode modificá-lo prontamente para devorar o restante da linha de dados, se preferir:

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

O problema residual é como relatar os três estados de resultado diferentes - EOF ou erro, linha lida e não truncada e linha parcial lida, mas os dados foram truncados.

Esse problema não ocorre gets()porque ele não sabe onde seu buffer termina e atropela alegremente além do fim, causando estragos em seu layout de memória bem cuidado, geralmente atrapalhando a pilha de retorno (um estouro de pilha ) se o buffer estiver alocado em a pilha ou pisar nas informações de controle se o buffer for alocado dinamicamente ou copiar dados sobre outras variáveis ​​globais preciosas (ou módulos) se o buffer for alocado estaticamente. Nada disso é uma boa idéia - eles resumem a frase "comportamento indefinido".


Há também o TR 24731-1 (Relatório Técnico do Comitê Padrão C) que fornece alternativas mais seguras para uma variedade de funções, incluindo gets():

§6.5.4.1 A gets_sfunção

Sinopse

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

Restrições de tempo de execução

snão deve ser um ponteiro nulo. nnão deve ser igual a zero nem maior que RSIZE_MAX. Um caractere de nova linha, fim de arquivo ou erro de leitura deve ocorrer dentro dos n-1caracteres de leitura de stdin. 25)

3 Se houver uma violação de restrição de tempo de execução, s[0]for definido como o caractere nulo, e os caracteres serão lidos e descartados stdinaté que um caractere de nova linha seja lido, ou no final do arquivo ou ocorra um erro de leitura.

Descrição

4 A gets_sfunção lê no máximo um a menos do que o número de caracteres especificado por n do fluxo apontado por stdin, na matriz apontada por s. Nenhum caractere adicional é lido após um caractere de nova linha (que é descartado) ou após o final do arquivo. O caractere de nova linha descartado não conta para o número de caracteres lidos. Um caractere nulo é gravado imediatamente após o último caractere lido na matriz.

5 Se o final do arquivo for encontrado e nenhum caractere tiver sido lido na matriz ou se ocorrer um erro de leitura durante a operação, ele s[0]será definido como o caractere nulo e os outros elementos de svalores não especificados.

Prática recomendada

6 A fgetsfunção permite que programas escritos corretamente processem com segurança as linhas de entrada por tempo demais para armazenar na matriz de resultados. Em geral, isso exige que os chamadores fgetsprestem atenção à presença ou ausência de um caractere de nova linha na matriz de resultados. Considere usar fgets(juntamente com qualquer processamento necessário com base em caracteres de nova linha) em vez de gets_s.

25) A gets_sfunção, diferentemente gets, torna uma violação de restrição de tempo de execução uma linha de entrada exceder o buffer para armazená-lo. Ao contrário fgets, gets_smantém um relacionamento individual entre as linhas de entrada e as chamadas bem-sucedidas para gets_s. Os programas que usam getsesperam esse relacionamento.

Os compiladores do Microsoft Visual Studio implementam uma aproximação ao padrão TR 24731-1, mas há diferenças entre as assinaturas implementadas pela Microsoft e as do TR.

A norma C11, ISO / IEC 9899-2011, inclui TR24731 no anexo K como parte opcional da biblioteca. Infelizmente, raramente é implementado em sistemas similares ao Unix.


getline() - POSIX

O POSIX 2008 também fornece uma alternativa segura para gets()chamada getline(). Como aloca espaço para a linha dinamicamente, você acaba liberando-a. Remove a limitação no comprimento da linha, portanto. Ele também retorna o comprimento dos dados que foram lidos, ou -1(e não EOF!), O que significa que bytes nulos na entrada podem ser manipulados de maneira confiável. Há também uma variação de 'escolha seu próprio delimitador de caractere único' chamada getdelim(); isso pode ser útil se você estiver lidando com a saída de find -print0onde as extremidades dos nomes de arquivo são marcadas com um caractere ASCII NUL '\0', por exemplo.

Jonathan Leffler
fonte
8
Também vale ressaltar que, fgets()e sua fgets_wrapper()versão deixará a parte à direita de uma linha longa no buffer de entrada, para ser lida pela próxima função de entrada. Em muitos casos, convém ler e descartar esses caracteres.
Caf
5
Eu me pergunto por que eles não adicionaram uma alternativa fgets () que permite usar sua funcionalidade sem ter que fazer uma chamada boba. Por exemplo, uma variante fgets que retornou o número de bytes lidos na string facilitaria ao código ver se o último byte lido era uma nova linha. Se o comportamento de passar um ponteiro nulo para o buffer fosse definido como "ler e descartar até n-1 bytes até a próxima nova linha", isso permitiria que o código descartasse facilmente a cauda de linhas longas.
Supercat 27/03
2
@ supercat: Sim, eu concordo - é uma pena. A abordagem mais próxima disso provavelmente é POSIX getline()e seu parente getdelim(), que retornam o comprimento da 'linha' lida pelos comandos, alocando espaço conforme necessário para poder armazenar toda a linha. Mesmo isso pode causar problemas se você terminar com um arquivo JSON de linha única com vários gigabytes de tamanho; você pode pagar toda essa memória? (E enquanto estamos no assunto, podemos ter strcpy()e strcat()variantes que retornam um ponteiro para o byte nulo no final Etc.?)
Jonathan Leffler
4
@ supercat: o outro problema fgets()é que, se o arquivo contiver um byte nulo, você não poderá dizer quantos dados existem após o byte nulo até o final da linha (ou EOF). strlen()só pode relatar até o byte nulo nos dados; depois disso, é adivinhação e, portanto, quase certamente errado.
Jonathan Leffler 27/03
7
"esqueça que você já ouviu falar que gets()existia." Quando faço isso, encontro-o novamente e volto aqui. Você está invadindo o stackoverflow para obter votos positivos?
candied_orange
21

Porque getsnão faz nenhum tipo de verificação ao obter bytes do stdin e colocá-los em algum lugar. Um exemplo simples:

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

gets(array1);

Agora, primeiro, você tem permissão para inserir quantos caracteres deseja, getsnão se importando com isso. Em segundo lugar, os bytes acima do tamanho da matriz em que você os coloca (neste caso array1) substituirão o que encontrarem na memória, porque getsos escreverão. No exemplo anterior, isso significa que, se você digitar "abcdefghijklmnopqrts"talvez, imprevisivelmente, ele substituirá também array2ou o que for.

A função é insegura porque assume uma entrada consistente. NUNCA USE!

Jack
fonte
3
O que torna getstotalmente inutilizável é que ele não possui um parâmetro de comprimento / contagem de matriz necessário; se estivesse lá, seria apenas outra função padrão C comum.
precisa saber é o seguinte
@ legends2k: Estou curioso para saber qual foi o uso pretendido getse por que nenhuma variante padrão de fgets foi feita como conveniente para casos de uso em que a nova linha não é desejada como parte da entrada?
28815
1
@supercat getsfoi, como o nome sugere, projetado para obter uma string stdin, no entanto, a lógica de não ter um parâmetro de tamanho pode ter sido do espírito de C : confie no programador. Essa função foi removida no C11 e a substituição fornecida gets_sleva o tamanho do buffer de entrada. Eu não tenho idéia sobre a fgetsparte.
Legends2k 29/03/2015
@ legends2k: O único contexto que eu posso ver em que getspoderia ser desculpável seria se alguém estivesse usando um sistema de E / S com buffer de linha de hardware que fosse fisicamente incapaz de enviar uma linha por um determinado comprimento e a vida útil pretendida do programa foi menor que a vida útil do hardware. Nesse caso, se o hardware for incapaz de enviar linhas com mais de 127 bytes, pode ser justificável getsinserir um buffer de 128 bytes, embora eu ache que as vantagens de poder especificar um buffer menor ao esperar uma entrada menor seriam mais do que justificáveis. custo.
Supercat 29/03
@ legends2k: Na verdade, o que poderia ter sido ideal seria ter um "ponteiro de string" para identificar um byte que selecionaria entre alguns formatos diferentes de string / buffer / buffer-info, com um valor de prefixo byte indicando uma estrutura que continha o prefixo byte [mais preenchimento], além do tamanho do buffer, tamanho usado e endereço do texto real. Esse padrão tornaria possível que o código passasse uma substring arbitrária (não apenas a cauda) de outra string sem ter que copiar nada e permitiria métodos como getse strcataceitar com segurança o que couber.
Supercat 29/03
16

Você não deve usar, getspois não há como interromper um estouro de buffer. Se o usuário digitar mais dados do que pode caber no seu buffer, é provável que você acabe com corrupção ou algo pior.

De fato, a ISO realmente tomou o passo de remover gets do padrão C (a partir do C11, apesar de ter sido preterido no C99) que, dada a alta taxa de compatibilidade com versões anteriores, deveria ser uma indicação de quão ruim era essa função.

O correto é usar a fgetsfunção com o stdinidentificador de arquivo, pois você pode limitar os caracteres lidos pelo usuário.

Mas isso também tem problemas como:

  • caracteres extras inseridos pelo usuário serão capturados na próxima vez.
  • não há notificação rápida de que o usuário inseriu muitos dados.

Para esse fim, quase todo codificador C em algum momento de sua carreira também escreverá um invólucro mais útil fgets. Aqui está o meu:

#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;
}

com algum código de teste:

// 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;
}

Ele fornece as mesmas proteções fgetsque evita o estouro de buffer, mas também notifica o chamador sobre o que aconteceu e limpa os caracteres em excesso para que eles não afetem sua próxima operação de entrada.

Sinta-se livre para usá-lo como desejar, por meio deste, a libero sob a licença "faça o que você quiser" :-)

paxdiablo
fonte
Na verdade, o padrão C99 original não foi reprovado explicitamente gets()na seção 7.19.7.7, onde está definido, nem na seção 7.26.9 Instruções futuras da biblioteca e na subseção para <stdio.h>. Não há sequer uma nota de rodapé sobre ser perigoso. (Dito isto, vejo "Está obsoleto na ISO / IEC 9899: 1999 / Cor.3: 2007 (E))" na resposta de Yu Hao .) Mas o C11 o removeu do padrão - e não antes do tempo!
Jonathan Leffler
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)esconde size_tà intconversão de sz. sz > INT_MAX || sz < 2pegaria valores estranhos de sz.
chux - Reinstala Monica
if (buff[strlen(buff)-1] != '\n') {é uma exploração hacker, pois o primeiro caractere do usuário mal digitado pode ser um caractere nulo incorporado que renderiza buff[strlen(buff)-1]UB. while (((ch = getchar())...tem problemas caso um usuário insira um caractere nulo.
chux - Reinstala Monica
12

objetos .

Para ler a partir do stdin:

char string[512];

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

Você não pode remover funções da API sem interromper a API. Se você preferir, muitos aplicativos não serão mais compilados ou executados.

Esta é a razão que uma referência fornece:

A leitura de uma linha que transborda a matriz apontada por s resulta em um comportamento indefinido. O uso de fgets () é recomendado.

Gerd Klima
fonte
4

Li recentemente, em um post da USENETcomp.lang.c , que gets()está sendo removido do Padrão. WOOHOO

Você ficará feliz em saber que o comitê acabou de votar (por unanimidade, como se vê) para remover os gets () do rascunho também.

pmg
fonte
3
É excelente que esteja sendo removido do padrão. No entanto, a maioria das implementações o fornecerá como uma "extensão agora fora do padrão" pelos próximos 20 anos, devido à compatibilidade com versões anteriores.
9786 Jonathan Leffler #
1
Sim, certo, mas quando você compila com gcc -std=c2012 -pedantic ...gets () não será concluído. (Eu só fiz o -stdparâmetro)
pmg
4

Em C11 (ISO / IEC 9899: 201x), gets()foi removido. (Foi descontinuado na ISO / IEC 9899: 1999 / Cor.3: 2007 (E))

Além disso fgets(), o C11 apresenta uma nova alternativa segura gets_s():

C11 K.3.5.4.1 A gets_sfunção

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

No entanto, na seção Prática recomendada , fgets()ainda é preferível.

A fgetsfunção permite que programas gravados corretamente processem com segurança as linhas de entrada por muito tempo para armazenar na matriz de resultados. Em geral, isso exige que os chamadores fgetsprestem atenção à presença ou ausência de um caractere de nova linha na matriz de resultados. Considere usar fgets(juntamente com qualquer processamento necessário com base em caracteres de nova linha) em vez de gets_s.

Yu Hao
fonte
3

gets()é perigoso porque é possível ao usuário travar o programa digitando muito no prompt. Ele não pode detectar o fim da memória disponível; portanto, se você alocar uma quantidade de memória muito pequena para esse objetivo, poderá causar uma falha e travamento de seg. Às vezes, parece muito improvável que um usuário digite 1000 letras em um prompt destinado ao nome de uma pessoa, mas como programadores, precisamos tornar nossos programas à prova de balas. (também pode ser um risco de segurança se um usuário travar um programa do sistema enviando muitos dados).

fgets() permite especificar quantos caracteres são retirados do buffer de entrada padrão, para que eles não excedam a variável.

Aradhana Mohanty
fonte
Observe que o perigo real não está em travar seu programa, mas em fazê-lo executar um código arbitrário . (Em geral, explorando um comportamento indefinido .)
Tanz87
2

Eu gostaria de estender um convite sincero a qualquer mantenedor de biblioteca C por aí que ainda esteja incluindo getsem suas bibliotecas "apenas no caso de alguém ainda depender disso": substitua sua implementação pelo equivalente a

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

Isso ajudará a garantir que ninguém ainda esteja dependendo disso. Obrigado.

Steve Summit
fonte
2

OC recebe a função é perigosa e tem sido um erro muito caro. Tony Hoare o destaca para menção específica em sua palestra "Null References: The Billion Dollar Mistake":

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

Vale a pena assistir a hora inteira, mas, pelos comentários dele, a partir de 30 minutos, o crítico recebe críticas em torno de 39 minutos.

Espero que isso apimente seu apetite por toda a conversa, o que chama a atenção para como precisamos de provas formais de correção em idiomas e como os designers de idiomas devem ser responsabilizados pelos erros em seus idiomas, não pelo programador. Essa parece ter sido a razão dúbia dos designers de linguagens ruins colocarem a culpa nos programadores sob o disfarce de "liberdade do programador".

user3717661
fonte