¿Por qué la función gets es tan peligrosa que no debería usarse?

229

Cuando intento compilar el código C que usa la gets()función con GCC, aparece esta advertencia:

(.text + 0x34): advertencia: la función 'gets' es peligrosa y no debe usarse.

Recuerdo que esto tiene algo que ver con la protección y seguridad de la pila, pero no estoy seguro exactamente por qué.

¿Cómo puedo eliminar esta advertencia y por qué hay tal advertencia sobre el uso gets()?

Si gets()es tan peligroso, ¿por qué no podemos eliminarlo?

vinit dhatrak
fuente

Respuestas:

179

Para usarlo de getsforma segura, debe saber exactamente cuántos caracteres leerá, de modo que pueda hacer que su búfer sea lo suficientemente grande. Solo lo sabrá si sabe exactamente qué datos leerá.

En lugar de usar gets, desea usar fgets, que tiene la firma

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

( fgetssi lee una línea completa, la dejará '\n'en la cadena; tendrá que lidiar con eso).

Seguía siendo una parte oficial del lenguaje hasta el estándar ISO C de 1999, pero fue eliminado oficialmente por el estándar de 2011. La mayoría de las implementaciones de C todavía lo admiten, pero al menos gcc emite una advertencia para cualquier código que lo use.

Thomas Owens
fuente
79
En realidad, no es gcc lo que advierte, es el glibc que contiene un pragma o atributo gets()que hace que el compilador emita una advertencia cuando se usa.
fuz
@fuz en realidad, ni siquiera es el compilador el que advierte: ¡la advertencia citada en el OP fue impresa por el enlazador!
Ruslan
163

Porque es gets()peligroso

El primer gusano de internet ( Morris Internet Worm ) escapó hace unos 30 años (1988-11-02), y usó gets()un desbordamiento de búfer como uno de sus métodos de propagación de un sistema a otro. El problema básico es que la función no sabe qué tan grande es el búfer, por lo que continúa leyendo hasta que encuentra una nueva línea o encuentra EOF, y puede desbordar los límites del búfer que se le dio.

Deberías olvidar que alguna vez escuchaste que gets()existía.

El estándar C11 ISO / IEC 9899: 2011 se eliminó gets()como una función estándar, que es A Good Thing ™ (se marcó formalmente como 'obsoleto' y 'obsoleto' en ISO / IEC 9899: 1999 / Cor.3: 2007 - Corrigendum técnico 3 para C99, y luego eliminado en C11). Lamentablemente, permanecerá en las bibliotecas durante muchos años (lo que significa 'décadas') por razones de compatibilidad con versiones anteriores. Si fuera por mí, la implementación de gets()se convertiría en:

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

Dado que su código se bloqueará de todos modos, tarde o temprano, es mejor evitar el problema más temprano que tarde. Estaría preparado para agregar un mensaje de error:

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

Las versiones modernas del sistema de compilación de Linux generan advertencias si se vincula gets(), y también para algunas otras funciones que también tienen problemas de seguridad ( mktemp(), ...).

Alternativas a gets()

fgets ()

Como todos los demás dijeron, la alternativa canónica gets()es fgets()especificar stdincomo la secuencia del archivo.

char buffer[BUFSIZ];

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

Lo que nadie más mencionó es que gets()no incluye la nueva línea, pero fgets()sí. Por lo tanto, es posible que deba usar un contenedor fgets()que elimine la nueva línea:

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

O mejor:

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

Además, como caf señala en un comentario y paxdiablo muestra en su respuesta, fgets()es posible que le queden datos en una línea. Mi código contenedor deja esos datos para leer la próxima vez; puede modificarlo fácilmente para engullir el resto de la línea de datos si lo prefiere:

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

El problema residual es cómo informar los tres estados de resultados diferentes: EOF o error, lectura de línea y no truncada, y lectura de línea parcial pero los datos se truncaron.

Este problema no surge gets()porque no sabe dónde termina su búfer y se aleja alegremente más allá del final, causando estragos en su diseño de memoria bellamente cuidado, a menudo estropeando la pila de retorno (un desbordamiento de pila ) si el búfer está asignado en la pila, o pisotear la información de control si el búfer se asigna dinámicamente, o copiar datos sobre otras valiosas variables globales (o módulos) si el búfer se asigna estáticamente. Ninguno de estos es una buena idea: personifican la frase 'comportamiento indefinido'.


También existe el TR 24731-1 (Informe Técnico del Comité Estándar de C) que proporciona alternativas más seguras a una variedad de funciones, que incluyen gets():

§6.5.4.1 La gets_sfunción

Sinopsis

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

Restricciones de tiempo de ejecución

sno será un puntero nulo. nno será igual a cero ni mayor que RSIZE_MAX. Se producirá un n-1carácter de nueva línea, fin de archivo o error de lectura dentro de la lectura de caracteres de stdin. 25)

3 Si hay una violación de restricción de tiempo de ejecución, s[0]se establece en el carácter nulo, y los caracteres se leen y descartan stdinhasta que se lee un carácter de nueva línea, o al final del archivo o se produce un error de lectura.

Descripción

4 La gets_sfunción lee como máximo uno menos que el número de caracteres especificados por n del flujo al que apunta stdin, en la matriz a la que apunta s. No se leen caracteres adicionales después de un carácter de nueva línea (que se descarta) o después del final del archivo. El carácter de nueva línea descartado no cuenta para el número de caracteres leídos. Se escribe un carácter nulo inmediatamente después del último carácter leído en la matriz.

5 Si se encuentra el final del archivo y no se han leído caracteres en la matriz, o si se produce un error de lectura durante la operación, s[0]se establece en el carácter nulo y los demás elementos stoman valores no especificados.

Práctica recomendada

6 La fgetsfunción permite que los programas escritos correctamente procesen con seguridad las líneas de entrada durante demasiado tiempo para almacenarlas en la matriz de resultados. En general, esto requiere que las personas que llaman fgetspresten atención a la presencia o ausencia de un carácter de nueva línea en la matriz de resultados. Considere usar fgets(junto con cualquier procesamiento necesario basado en caracteres de nueva línea) en lugar de gets_s.

25) La gets_sfunción, a diferencia gets, la convierte en una violación de restricción de tiempo de ejecución para que una línea de entrada desborde el búfer para almacenarla. A diferencia fgets, gets_smantiene una relación uno a uno entre las líneas de entrada y las llamadas exitosas a gets_s. Los programas que usan getsesperan tal relación.

Los compiladores de Microsoft Visual Studio implementan una aproximación al estándar TR 24731-1, pero existen diferencias entre las firmas implementadas por Microsoft y las del TR.

El estándar C11, ISO / IEC 9899-2011, incluye TR24731 en el Anexo K como una parte opcional de la biblioteca. Desafortunadamente, rara vez se implementa en sistemas similares a Unix.


getline() - POSIX

POSIX 2008 también proporciona una alternativa segura a la gets()llamada getline(). Asigna espacio para la línea dinámicamente, por lo que terminará necesitando liberarla. Elimina la limitación en la longitud de la línea, por lo tanto. También devuelve la longitud de los datos que se leyeron, o -1(¡y no EOF!), Lo que significa que los bytes nulos en la entrada se pueden manejar de manera confiable. También hay una variación 'elige tu propio delimitador de un solo carácter' llamada getdelim(); Esto puede ser útil si se trata de la salida desde find -print0donde los extremos de los nombres de archivo están marcados con un carácter ASCII NUL '\0', por ejemplo.

Jonathan Leffler
fuente
8
También vale la pena señalar que fgets()y su fgets_wrapper()versión dejará la parte posterior de una línea demasiado larga en el búfer de entrada, para que la lea la siguiente función de entrada. En muchos casos, querrá leer y descartar estos caracteres.
caf
55
Me pregunto por qué no agregaron una alternativa fgets () que le permita a uno usar su funcionalidad sin tener que hacer una llamada tonta. Por ejemplo, una variante de fgets que devuelve el número de bytes leídos en la cadena facilitaría que el código vea si el último byte leído fue una nueva línea. Si el comportamiento de pasar un puntero nulo para el búfer se definió como "leer y descartar hasta n-1 bytes hasta la próxima línea nueva", eso permitiría que el código descarte fácilmente la cola de las líneas de longitud excesiva.
supercat
2
@supercat: Sí, estoy de acuerdo, es una pena. El enfoque más cercano a eso es probablemente POSIX getline()y su relativo getdelim(), que devuelve la longitud de la 'línea' leída por los comandos, asignando espacio según sea necesario para poder almacenar toda la línea. Incluso eso puede causar problemas si termina con un archivo JSON de una sola línea que tiene un tamaño de varios gigabytes; ¿Puedes permitirte todo ese recuerdo? (Y ya que estamos en ello, podemos tener strcpy()y strcat()variantes que devuelven un puntero al byte nulo al final etc.?)
Jonathan Leffler
44
@supercat: el otro problema fgets()es que si el archivo contiene un byte nulo, no puede saber cuántos datos hay después del byte nulo hasta el final de la línea (o EOF). strlen()solo puede informar hasta el byte nulo en los datos; después de eso, es una conjetura y, por lo tanto, casi seguro que está mal.
Jonathan Leffler
77
"olvide que alguna vez escuchó que gets()existía". Cuando hago esto, me encuentro de nuevo y regreso aquí. ¿Estás pirateando stackoverflow para obtener votos positivos?
candied_orange
21

Porque getsno hace ningún tipo de verificación mientras obtiene bytes de stdin y los coloca en algún lugar. Un simple ejemplo:

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

gets(array1);

Ahora, en primer lugar, se le permite ingresar la cantidad de caracteres que desea, getsno le importará. En segundo lugar, los bytes superiores al tamaño de la matriz en la que los coloca (en este caso array1) sobrescribirán lo que encuentren en la memoria porque getslos escribirá. En el ejemplo anterior, esto significa que si ingresa "abcdefghijklmnopqrts"tal vez, de manera impredecible, también se sobrescribirá array2o lo que sea.

La función no es segura porque supone una entrada consistente. ¡NUNCA LO USE!

Jack
fuente
3
Lo que hace getscompletamente inutilizable es que no tiene un parámetro de longitud / conteo de matriz que tome; Si hubiera estado allí, sería otra función estándar de C.
legends2k
@ legends2k: Tengo curiosidad por saber cuál fue el uso previsto getsy por qué no se hizo una variante de fgets estándar como conveniente para los casos de uso en los que no se desea la nueva línea como parte de la entrada.
supercat
1
@supercat getsfue, como su nombre lo indica, diseñado para obtener una cadena stdin, sin embargo, la razón para no tener un parámetro de tamaño puede haber sido del espíritu de C : Confíe en el programador. Esta función se eliminó en C11 y el reemplazo dado gets_stoma el tamaño del búfer de entrada. Sin fgetsembargo, no tengo idea de la parte.
legends2k
@ legends2k: El único contexto que puedo ver en el que getspodría ser excusable sería si uno estuviera usando un sistema de E / S con buffer de línea de hardware que fuera físicamente incapaz de enviar una línea de cierta longitud, y la vida útil prevista del programa fue más corto que la vida útil del hardware. En ese caso, si el hardware es incapaz de enviar líneas de más de 127 bytes de longitud, podría justificarse getsen un búfer de 128 bytes, aunque creo que las ventajas de poder especificar un búfer más corto cuando se espera una entrada más pequeña justificaría más que costo.
supercat
@ legends2k: En realidad, lo que podría haber sido ideal habría sido tener un "puntero de cadena" que identificara un byte que seleccionara entre algunos formatos diferentes de información de cadena / buffer / buffer, con un valor de byte de prefijo que indica una estructura que contenía el prefijo byte [más relleno], más el tamaño del búfer, el tamaño utilizado y la dirección del texto real. Tal patrón permitiría que el código pase una subcadena arbitraria (no solo la cola) de otra cadena sin tener que copiar nada, y permitiría métodos como getsy strcataceptar con seguridad tanto como quepa.
supercat
16

No debe usarlo getsya que no tiene forma de detener un desbordamiento del búfer. Si el usuario ingresa más datos de los que caben en su búfer, lo más probable es que termine con corrupción o algo peor.

De hecho, ISO realmente ha dado el paso de eliminar gets del estándar C (a partir de C11, aunque fue obsoleto en C99) que, dada la alta calificación que tienen de la compatibilidad con versiones anteriores, debería ser una indicación de cuán mala era esa función.

Lo correcto es utilizar la fgetsfunción con el stdinidentificador de archivo, ya que puede limitar los caracteres leídos por el usuario.

Pero esto también tiene problemas como:

  • los caracteres adicionales ingresados ​​por el usuario serán recogidos la próxima vez.
  • no hay una notificación rápida de que el usuario ingresó demasiados datos.

Con ese fin, casi todos los codificadores C en algún momento de su carrera también escribirán un contenedor más útil fgets. Aquí está el mío:

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

con algún código de prueba:

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

Proporciona las mismas protecciones, ya fgetsque evita el desbordamiento del búfer, pero también notifica a la persona que llama lo que sucedió y borra el exceso de caracteres para que no afecten a su próxima operación de entrada.

Siéntase libre de usarlo como lo desee, por la presente lo libero bajo la licencia "haz lo que quieras" :-)

paxdiablo
fuente
En realidad, el estándar C99 original no se desaprobó explícitamente gets()ni en la sección 7.19.7.7 donde se define ni en la sección 7.26.9 Direcciones futuras de la biblioteca y la subsección para <stdio.h>. Ni siquiera hay una nota al pie sobre que sea peligroso. (Dicho esto, veo "Está en desuso en ISO / IEC 9899: 1999 / Cor.3: 2007 (E))" en la respuesta de Yu Hao .) ¡Pero C11 lo eliminó del estándar, y no antes de tiempo!
Jonathan Leffler
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)oculta la size_tde intconversión de sz. sz > INT_MAX || sz < 2atraparía valores extraños de sz.
chux - Restablece a Mónica el
if (buff[strlen(buff)-1] != '\n') {es un exploit hacker, ya que el primer carácter del usuario maligno ingresado podría ser un carácter nulo incrustado que representa buff[strlen(buff)-1]UB. while (((ch = getchar())...tiene problemas si un usuario ingresa un carácter nulo.
chux - Restablece a Mónica el
12

Fgets .

Para leer del stdin:

char string[512];

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

No puede eliminar las funciones de API sin romper la API. Si lo hiciera, muchas aplicaciones ya no se compilarían ni ejecutarían en absoluto.

Esta es la razón que da una referencia :

La lectura de una línea que desborda la matriz a la que apunta s da como resultado un comportamiento indefinido. Se recomienda el uso de fgets ().

Gerd Klima
fuente
4

Leí recientemente, en una publicación de USENETcomp.lang.c , que gets()se está eliminando del Estándar. WOOHOO

Te alegrará saber que el comité acaba de votar (por unanimidad, como resultado) para eliminar también gets () del borrador.

pmg
fuente
3
Es excelente que se elimine del estándar. Sin embargo, la mayoría de las implementaciones lo proporcionarán como una 'extensión ahora no estándar' durante al menos los próximos 20 años, debido a la compatibilidad con versiones anteriores.
Jonathan Leffler el
1
Sí, claro, pero cuando compilas con gcc -std=c2012 -pedantic ...gets () no se logrará. (Acabo de inventar el -stdparámetro)
pmg
4

En C11 (ISO / IEC 9899: 201x), gets()se ha eliminado. (Está en desuso en ISO / IEC 9899: 1999 / Cor.3: 2007 (E))

Además de fgets(), C11 presenta una nueva alternativa segura gets_s():

C11 K.3.5.4.1 La gets_sfunción

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

Sin embargo, en la sección Práctica recomendada , fgets()todavía se prefiere.

La fgetsfunción permite que los programas escritos correctamente procesen con seguridad las líneas de entrada durante demasiado tiempo para almacenarlas en la matriz de resultados. En general, esto requiere que las personas que llaman fgetspresten atención a la presencia o ausencia de un carácter de nueva línea en la matriz de resultados. Considere usar fgets(junto con cualquier procesamiento necesario basado en caracteres de nueva línea) en lugar de gets_s.

Yu Hao
fuente
3

gets()es peligroso porque es posible que el usuario bloquee el programa escribiendo demasiado en el indicador. No puede detectar el final de la memoria disponible, por lo que si asigna una cantidad de memoria demasiado pequeña para el propósito, puede causar una falla de seguridad y bloquearse. A veces parece muy poco probable que un usuario escriba 1000 letras en un mensaje destinado al nombre de una persona, pero como programadores, necesitamos que nuestros programas sean a prueba de balas. (También puede ser un riesgo de seguridad si un usuario puede bloquear un programa del sistema al enviar demasiados datos).

fgets() le permite especificar cuántos caracteres se sacan del búfer de entrada estándar, para que no sobrepasen la variable.

Aradhana Mohanty
fuente
Tenga en cuenta que el peligro real no está en poder bloquear su programa, sino en poder ejecutar un código arbitrario . (En general, explotar el comportamiento indefinido .)
Tanz87
2

Me gustaría extender una invitación sincera a todos los encargados del mantenimiento de bibliotecas C que todavía están incluidos getsen sus bibliotecas "en caso de que alguien todavía dependa de ello": Reemplace su implementación con el equivalente de

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

Esto ayudará a asegurar que nadie siga dependiendo de ello. Gracias.

Steve Summit
fuente
2

La función C gets es peligrosa y ha sido un error muy costoso. Tony Hoare lo destaca por su mención específica en su charla "Null References: The Billion Dollar Mistake":

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

Vale la pena ver toda la hora, pero por sus comentarios, la vista a partir de los 30 minutos en adelante con el específico recibe críticas alrededor de los 39 minutos.

Con suerte, esto abrirá su apetito por toda la charla, que llama la atención sobre cómo necesitamos pruebas de corrección más formales en los idiomas y cómo se debe culpar a los diseñadores de idiomas por los errores en sus idiomas, no al programador. Esta parece haber sido la razón dudosa para que los diseñadores de lenguajes malos echen la culpa a los programadores con el pretexto de "libertad del programador".

usuario3717661
fuente