Regla 0x5004h: Todas las operaciones con cadenas deben ser seguras
Compilacion, preprocesador y seguridad (0x50XX)
0x5004h: Todas las operaciones con cadenas deben ser seguras¶
Enunciado normativo¶
DEBEN usarse funciones de cadena que acoten el tamaño del destino (
snprintf,strncpy,strncat) o que operen sobre longitudes conocidas. NO DEBEN usarsestrcpy,strcatnisprintfsin control de límite.
¿Por qué existe esta regla?¶
El problema¶
En C una cadena es un char[] terminado en '\0'; ninguna función sabe cuánto
mide el buffer de destino. strcpy y strcat copian hasta encontrar el nulo de
la fuente, así que si la fuente es más larga que el destino escriben más
allá del final: desbordamiento de búfer. sprintf hace lo mismo con el texto
formateado, y su longitud no se conoce hasta evaluar el formato en runtime.
El desbordamiento de pila es explotable: pisa variables locales y la dirección
de retorno, y puede ejecutar código arbitrario (stack smashing). Aun sin
atacante, corrompe memoria y el programa aborta con SIGABRT.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Comportamiento indefinido | Escritura fuera de límites: SIGSEGV, SIGABRT o corrupción silenciosa. |
| Vulnerabilidad | Desbordamiento de pila explotable; el programa puede ejecutar datos de entrada. |
| Bug silencioso | strncpy puede no terminar en '\0' y la cadena «sigue» leyendo basura. |
| Mantenibilidad | Cada copia insegura exige auditar el tamaño máximo a mano. |
Fundamento en el estándar y en la cátedra¶
ISO/IEC 9899:2011 §7.24 regula <string.h> y §7.21.6.6/§7.21.6.5 snprintf/
printf. strncpy se define en §7.24.2.4: copia a lo sumo n bytes y no
garantiza el terminador. La cátedra prohíbe las variantes sin límite porque el
código se evalúa en un sandbox con entradas adversarias.
Alcance y excepciones¶
Aplica a toda manipulación de cadenas, tanto de entrada como de salida.
Excepción: no aplica a buffers cuyo tamaño se valida con memcpy
(n <= sizeof(dest)). Comparar cadenas siempre con strcmp (o strncmp):
== compara direcciones, que es el defecto de 0x5004h.
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — strcpy/strcat sin límite¶
#include <string.h>
void componer(char *dest, const char *nombre)
{
strcpy(dest, "Usuario: ");
strcat(dest, nombre);
}Por qué falla: si nombre es más largo que dest, strcat escribe fuera del
buffer. El error no se detecta en la llamada sino en el uso posterior de memoria
vecina; con -D_FORTIFY_SOURCE=2 el programa aborta.
❌ Contraejemplo 2 — sprintf sin sizeof¶
#include <stdio.h>
void etiquetar(char *dest, const char *id, int codigo)
{
sprintf(dest, "ID: %s, Cod: %d", id, codigo);
}Por qué falla: el texto resultante depende de id, así que puede exceder el
destino sin que nada lo impida. Reemplazarlo por snprintf(dest, tam, ...)
obliga a declarar el tamaño real (0x5004h).
✅ Ejemplo conforme 1 — snprintf con sizeof¶
#include <stdio.h>
void etiquetar(char dest[], size_t tam, const char *id, int codigo)
{
snprintf(dest, tam, "ID: %s, Cod: %d", id, codigo);
}snprintf escribe a lo sumo tam - 1 caracteres y agrega '\0'. El valor de
retorno permite detectar truncamiento comparándolo con tam; pasarlo como
parámetro evita el error clásico de sizeof sobre un puntero.
✅ Ejemplo conforme 2 — copia acotada con terminación explícita y comparación correcta¶
#include <string.h>
void copiar(char *dest, size_t tam, const char *orig)
{
strncpy(dest, orig, tam - 1);
dest[tam - 1] = '\0';
}
int es_admin(const char *rol)
{
return strcmp(rol, "admin") == 0;
}strncpy no agrega el nulo si la fuente es más larga, por eso se fija
dest[tam - 1] a mano. La comparación usa strcmp == 0, que resuelve 0x5004h.
⚠️ Casos límite¶
tam == 0:snprintf(dest, 0, ...)es válido y sólo devuelve la longitud que habría necesitado; no escribir en un buffer de tamaño cero.sizeofsobre puntero: dentro devoid f(char *dest)no existe el tamaño del arreglo;sizeof(dest)da 8 y es un defecto, no una defensa.
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | Reporta 0x5004h ante strcpy, strcat, sprintf y == entre cadenas. |
gcc / clang | gcc -std=c11 -Wall -Wextra -Werror -pedantic -D_FORTIFY_SOURCE=2 archivo.c | warning: 'strcpy' writing N bytes into a region of size M. |
cppcheck | cppcheck --enable=warning archivo.c | Buffer is not null-terminated / String comparison. |
Checklist de autocontrol¶
¿Usé
snprintf/strncpy/strncatcon el tamaño del destino?¿Terminé a mano la cadena cuando usé
strncpy?¿Comparé cadenas con
strcmp, no con==?¿Pasé
size_t tamen lugar de confiar ensizeofsobre un puntero?¿El buffer de destino tiene espacio para el terminador?
Reglas relacionadas¶
0x5006h: Preferí fgets sobre gets y scanf para leer cadenas — preferí
fgetspara traer texto del usuario sin desbordar.0x5008h: Prohibición de funciones obsoletas o inseguras (gets, atoi) —
getsyatoiestán prohibidos por la misma razón de fondo.0x500Bh: Inclusión obligatoria de cabeceras estándar para funciones de la biblioteca C — incluí
<string.h>y<stdio.h>para que el compilador valide las firmas.0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos — validá límites antes de indexar; complementa la seguridad de cadenas.
Antipatrón: Comparación directa de cadenas con == o !=¶
Síntoma en el código del estudiante¶
Aparece un operador == o != comparando un char * contra un literal
entrecomillado, o contra otro char *:
if (nombre == "admin")
if (opcion != "salir")A simple vista parece una comparación de texto, y ahí está la trampa: el operador compara direcciones de memoria, no caracteres.
Diagnóstico¶
Mecanismo del defecto¶
En una expresión, un arreglo char nombre[16] y un literal "admin"
decaen a char * apuntando a su primer carácter (C11 §6.3.2.1p3). El
operador == sobre punteros compara los valores de esas direcciones, no el
contenido apuntado. El literal "admin" vive en una región de solo lectura
del binario; el búfer nombre vive en la pila o en el heap. Son objetos
distintos, de modo que nombre == "admin" es falso aunque el texto coincida
letra por letra.
Agrega una vuelta de tuerca que el estándar no obliga a que dos
literales idénticos ocupen direcciones distintas (C11 §6.4.5p6): así,
"hola" == "hola" puede dar 1 o 0 según el compilador y los flags. El
resultado es no especificado, no simplemente falso.
Consecuencia observable¶
El
ifde validación de usuario nunca entra: el login siempre rechaza.Un lazo
while (comando != "fin")no termina nunca, o termina al revés.No hay error de compilación; el bug es silencioso y se disfraza de lógica invertida. Con
-Wallsuele aparecer una advertencia de dirección.
Fundamento en el estándar C11¶
== sobre punteros compara identidad de objeto (C11 §6.5.9). Para comparar
el contenido lexicográfico de dos cadenas se usa strcmp (C11 §7.24.4.2),
que recorre ambos arreglos hasta el '\0' y devuelve 0 si son iguales, un
valor negativo si la primera precede a la segunda y uno positivo si la
sigue. La cátedra exige además comparar de forma explícita
(strcmp(...) == 0) en lugar de depender de la veracidad implícita del
entero, conforme a 0x1005h: Reemplazá las condiciones ambiguas basadas en la ‘veracidad’ (truthiness) del tipo de dato.
Corrección idiomática¶
❌ Código con el antipatrón¶
#include <stdio.h>
#include <string.h>
static int es_administrador(const char *usuario)
{
if (usuario == "admin") {
return 1;
}
return 0;
}✅ Código refactorizado¶
#include <stdio.h>
#include <string.h>
static int es_administrador(const char *usuario)
{
if (usuario == NULL) {
return 0;
}
return strcmp(usuario, "admin") == 0;
}Para orden alfabético se comparan los signos: strcmp(a, b) < 0 significa
que a precede a b. Para prefijos acotados se usa strncmp(a, b, n) == 0.
Errores típicos al compilar o ejecutar¶
aviso.c:6:19: warning: comparison with string literal results in unspecified behavior [-Waddress]
6 | if (usuario == "admin") {
| ^~
aviso.c:6:19: note: use 'strcmp' to compare stringsSin -Wall no hay diagnóstico y el programa simplemente valida mal.
Checklist de verificación¶
¿Algún
==/!=tiene unchar *o un literal entre comillas como operando?¿Uso
strcmp/strncmppara comparar contenido y no dirección?¿El resultado de
strcmpse compara explícitamente contra0?¿Verifiqué
NULLantes de pasar el puntero astrcmp?¿Compilé con
-Wall -Wextray no quedan advertencias-Waddress?
Reglas relacionadas¶
0x5004h: Todas las operaciones con cadenas deben ser seguras — norma general de operaciones seguras con cadenas.
0x1005h: Reemplazá las condiciones ambiguas basadas en la ‘veracidad’ (truthiness) del tipo de dato — exige comparaciones explícitas contra
0oNULL.0x5006h: Preferí fgets sobre gets y scanf para leer cadenas — la entrada de cadenas se lee con
fgets, no congets.
Antipatrón: Formateo inseguro con sprintf() sin comprobación de destino¶
Síntoma en el código del estudiante¶
Llamadas a sprintf cuyo primer argumento es un búfer de tamaño fijo y que
nunca reciben ese tamaño:
char salida[32];
sprintf(salida, "ID: %s, Cod: %d", id, codigo);El patrón se repite con strcpy y strcat, pero acá el vector es el
formateo: el largo final depende de los datos de entrada y el compilador no
lo puede acotar por sí solo.
Diagnóstico¶
Mecanismo del defecto¶
sprintf escribe el resultado formateado y agrega un '\0' final, pero
no conoce el tamaño de salida: recorre el formato y escribe tantos
bytes como produzca la expansión. Si %s apunta a una cadena de 40
caracteres y el destino mide 32, los últimos 9 bytes más el terminador se
escriben fuera del arreglo, sobre las variables vecinas o sobre la
dirección de retorno del marco de pila. En C11 esa escritura fuera de
límites es comportamiento indefinido (§6.5.6 y §7.1.4).
Si el formato proviniera del usuario, se agrega la vulnerabilidad de format
string: un %n o un %s inesperado altera la lectura o la escritura de la
pila.
Consecuencia observable¶
La compilación puede pasar limpia salvo advertencias de
-Wformat-overflow.En ejecución:
*** stack smashing detected ***: terminated,SIGABRToSIGSEGV.A veces no falla: corrompe datos en silencio, y el bug depende del largo de la entrada, por lo que aparece solo con los casos “largos”.
Si
idviene de afuera, es un desbordamiento de búfer explotable.
Fundamento en el estándar C11¶
La variante acotada es snprintf (C11 §7.21.6.5): recibe size_t n y
escribe a lo sumo n - 1 caracteres más el terminador, nunca desborda.
Devuelve la cantidad de caracteres que habrían sido necesarios; si ese valor
es negativo o mayor o igual que n, hubo truncamiento y hay que decidir qué
hacer (avisar, recortar o reintentar). sprintf (C11 §7.21.6.6) no ofrece
ninguna garantía de tamaño.
Corrección idiomática¶
❌ Código con el antipatrón¶
#include <stdio.h>
void formatear_mensaje(const char *id, int codigo)
{
char salida[32];
sprintf(salida, "ID: %s, Cod: %d", id, codigo);
printf("%s\n", salida);
}✅ Código refactorizado¶
#include <stdio.h>
void formatear_mensaje(const char *id, int codigo)
{
char salida[32];
int escritos = snprintf(salida, sizeof(salida), "ID: %s, Cod: %d", id, codigo);
if (escritos < 0 || (size_t)escritos >= sizeof(salida)) {
printf("Aviso: mensaje truncado\n");
return;
}
printf("%s\n", salida);
}Si el destino es dinámico o llega como parámetro, el largo no se obtiene
con sizeof: se pasa explícitamente como size_t, porque un char *
decaído mide solo el puntero (ver 0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado).
Errores típicos al compilar o ejecutar¶
*** stack smashing detected ***: terminated
Aborted (core dumped)Con advertencias altas, el compilador detecta el caso acotado:
aviso.c:6:5: warning: 'sprintf' output between 12 and 2147483658 bytes into a destination of size 32 [-Wformat-overflow=]Checklist de verificación¶
¿Todo formateo a búfer de tamaño fijo usa
snprintfconsizeof?¿Verifiqué el valor de retorno de
snprintfpara detectar truncamiento?¿Evité aplicar
sizeofsobre un puntero recibido como parámetro?¿El formato es un literal y no una cadena provista por el usuario?
¿Compilé con
-Wall -Wextra -Wformat-overflow?
Reglas relacionadas¶
0x5004h: Todas las operaciones con cadenas deben ser seguras — norma general sobre operaciones seguras con cadenas.
0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado — alocá con
sizeofdel objeto apuntado, no del puntero.0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos — verificá los límites antes de escribir un arreglo.
Antipatrón: Uso de funciones inseguras de manipulación de cadenas (strcpy/sprintf)¶
Síntoma en el código del estudiante¶
strcpy, strcat y sprintf aparecen sin ningún argumento de tamaño:
char destino[16];
strcpy(destino, nombre);
strcat(destino, apellido);
sprintf(destino, "%s: %d", nombre, valor);El patrón es siempre el mismo: la función descubre el largo del origen mientras copia, y el destino mide menos que el resultado.
Diagnóstico¶
Mecanismo del defecto¶
strcpy copia desde el origen hasta el '\0' inclusive, sin conocer la
capacidad de destino (C11 §7.24.2.3). strcat busca el terminador del
destino y agrega desde allí (C11 §7.24.3.1). Ambas escriben la cantidad de
bytes que dicte el origen. Cuando ese largo supera el del destino, la copia
pisa memoria ajena: variables contiguas, metadatos del heap o la dirección
de retorno del marco de pila. Es la definición misma de desbordamiento de
búfer y, en C11, comportamiento indefinido.
sprintf arrastra el mismo defecto amplificado: el largo depende del
formato y de los valores, y tampoco recibe límite.
Consecuencia observable¶
*** stack smashing detected ***/SIGABRT/SIGSEGVcuando el canario de pila detecta la escritura fuera de límites.Corrupción silenciosa cuando no hay canario: el programa “anda” con entradas cortas y falla con las largas.
Vector de ataque clásico: si el origen proviene de la entrada, permite redirigir el flujo de ejecución.
Fundamento en el estándar C11¶
Las funciones de la familia str* sin n no reciben tamaño y por eso no
pueden acotar (C11 §7.24.2). La alternativa acotada correcta es
snprintf(dest, sizeof(dest), "%s", src), que escribe a lo sumo
sizeof(dest) - 1 caracteres más el terminador (§7.21.6.5).
Cuidado: strncpy no es una versión segura de strcpy. Si el origen
tiene n o más caracteres, no agrega el '\0' (C11 §7.24.2.4) y el destino
queda sin terminar. Solo se usa cuando el terminador se agrega a mano.
Corrección idiomática¶
❌ Código con el antipatrón 1 — copia sin límite¶
#include <string.h>
void guardar_nombre(const char *nombre)
{
char copia[16];
strcpy(copia, nombre);
}❌ Código con el antipatrón 2 — concatenación sin límite¶
#include <string.h>
void guardar_completo(const char *nombre, const char *apellido)
{
char completo[16];
strcpy(completo, nombre);
strcat(completo, apellido);
}✅ Código refactorizado 1 — copia acotada¶
#include <stdio.h>
void guardar_nombre(const char *nombre)
{
char copia[16];
snprintf(copia, sizeof(copia), "%s", nombre);
}✅ Código refactorizado 2 — concatenación acotada¶
#include <stdio.h>
void guardar_completo(const char *nombre, const char *apellido)
{
char completo[16];
snprintf(completo, sizeof(completo), "%s%s", nombre, apellido);
}Cuando el destino es dinámico o llega como parámetro, el tamaño se pasa
explícitamente en lugar de usar sizeof.
Errores típicos al compilar o ejecutar¶
*** buffer overflow detected ***: terminated
Aborted (core dumped)Con -Wall el compilador avisa sobre los cruces detectables:
aviso.c:6:5: warning: 'strcpy' writing 32 bytes into a region of size 16 overflows the destination [-Wstringop-overflow=]Checklist de verificación¶
¿Aparece
strcpy,strcatosprintfen el código?¿Usé
snprintfcon elsizeofdel destino real?Si usé
strncpy, ¿terminé el destino con'\0'a mano?¿El tamaño se calcula sobre el arreglo y no sobre un puntero decaído?
¿Compilé con
-Wall -Wextra -Wstringop-overflow?
Reglas relacionadas¶
0x5004h: Todas las operaciones con cadenas deben ser seguras — norma general sobre operaciones seguras con cadenas.
0x5006h: Preferí fgets sobre gets y scanf para leer cadenas — usá
fgetspara leer cadenas desde la entrada.0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos — verificá límites antes de escribir un arreglo.