Seguridad: inyección SQL, contraseñas y copias
💡 🎯 Al terminar esta lección podrás…
- Reconocer y evitar la inyección SQL con consultas parametrizadas.
- Guardar contraseñas con hash y sal.
- Dar permisos mínimos con
GRANTyREVOKE. - Hacer y restaurar copias de seguridad en SQLite y PostgreSQL.
1.Qué es la inyección SQL
La inyección SQL ocurre cuando un programa construye una consulta pegando texto que escribió el usuario dentro del SQL. Si ese texto contiene comillas y palabras de SQL, el usuario deja de dar datos y empieza a escribir instrucciones. Es una de las vulnerabilidades más antiguas, más comunes y más dañinas de las aplicaciones web.
Imagina un formulario de acceso y una tabla de usuarios. (Para el ejemplo guardamos la clave en texto plano; en la sección 3 verás por qué no se debe hacer.)
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL UNIQUE,
clave TEXT NOT NULL,
rol TEXT NOT NULL DEFAULT 'cliente'
);
INSERT INTO usuarios (id, nombre, clave, rol) VALUES
(1, 'ana', 'clave123', 'cliente'),
(2, 'admin', 's3cret', 'administrador');
El código del servidor construye la consulta así (forma incorrecta):
const sql = "SELECT id, nombre, rol FROM usuarios WHERE nombre = '" + nombre + "' AND clave = '" + clave + "'";
Con un usuario honesto todo parece correcto:
SELECT id, nombre, rol FROM usuarios WHERE nombre = 'ana' AND clave = 'clave123'
| id | nombre | rol |
|---|---|---|
| 1 | ana | cliente |
Pero si el atacante escribe como nombre admin' --, el apóstrofo cierra el texto y -- convierte en comentario todo lo que sigue, incluida la comprobación de la clave:
SELECT id, nombre, rol FROM usuarios WHERE nombre = 'admin' --' AND clave = 'lo-que-sea'
| id | nombre | rol |
|---|---|---|
| 2 | admin | administrador |
Entró como administrador sin conocer su clave. Y con ' OR '1'='1 en ambos campos la condición se vuelve siempre verdadera:
SELECT id, nombre, rol FROM usuarios WHERE nombre = '' OR '1'='1' AND clave = '' OR '1'='1'
| id | nombre | rol |
|---|---|---|
| 1 | ana | cliente |
| 2 | admin | administrador |
⚠️ Importante · Puede ser peor
Si el motor o el driver admiten varias instrucciones en una sola llamada, un atacante puede añadir ; DROP TABLE usuarios; -- y borrar datos. La inyección también sirve para leer tablas completas que nunca debieron mostrarse.
2.La defensa: consultas parametrizadas
La solución es no mezclar nunca datos con instrucciones. En una consulta parametrizada el SQL se escribe con marcadores (? en SQLite, $1 en PostgreSQL) y los valores se envían aparte. El motor sabe que lo que llega en los parámetros es solo un dato, aunque contenga comillas o --.
SELECT id, nombre, rol
FROM usuarios
WHERE nombre = ? AND clave = ?;
Con el mismo ataque ' OR '1'='1 como nombre y clave, ahora el motor busca un usuario que se llame literalmente ' OR '1'='1:
(0 filas: acceso denegado)
Así se escribe en cada entorno:
const fila = db
.prepare('SELECT id, nombre, rol FROM usuarios WHERE nombre = ? AND clave_hash = ?')
.get(nombre, clave);
const { rows } = await pool.query(
'SELECT id, nombre, rol FROM usuarios WHERE nombre = $1',
[nombre]
);
⚠️ Importante · Qué NO se puede parametrizar
Los parámetros sustituyen valores, no nombres de tablas, de columnas ni palabras como ASC. Si necesitas elegir una columna para ordenar, compárala con una lista blanca escrita por ti (['precio', 'nombre']) y nunca la pegues tal como llega.
Otras defensas útiles, que no sustituyen a la anterior: validar el tipo y la longitud de los datos, no mostrar al usuario los mensajes de error de la base de datos y usar un rol con pocos permisos (sección 4).
3.Contraseñas: nunca en texto plano
Si alguien accede a tu base de datos (por una inyección, una copia perdida o un error de configuración) y las claves están en texto plano, todas las cuentas quedan expuestas, y como muchas personas repiten la clave en otros sitios, el daño se extiende. La solución es guardar solo un hash: el resultado de una función matemática de un solo sentido. Del hash no se puede recuperar la clave; para comprobar un acceso se calcula el hash de lo que escribe el usuario y se compara.
Un hash cambia por completo con una letra distinta. Estos son los SHA-256 reales de clave123 y clave124:
clave123 → 5ac0852e770506dcd80f1a36d20ba7878bf82244b836d9324593bd14bc56dcb5
clave124 → be287a79904331fa824b91a1bb762cb2c35d48088f60d8a722e426fb8af1c3ae
Pero SHA-256 no basta para contraseñas: es tan rápido que se pueden probar miles de millones de claves por segundo, y el mismo texto siempre da el mismo hash (los atacantes usan tablas ya calculadas). Para claves usa un algoritmo diseñado para ser lento y con sal aleatoria: bcrypt, scrypt o Argon2. La sal es un valor aleatorio distinto para cada clave, por lo que dos usuarios con la misma contraseña tienen hashes diferentes.
import bcrypt from 'bcrypt';
// Registro: guardar solo el hash
const hash = await bcrypt.hash(clave, 12);
await pool.query(
'INSERT INTO usuarios (nombre, clave_hash) VALUES ($1, $2)',
[nombre, hash]
);
// Acceso: buscar por nombre y comparar
const { rows } = await pool.query(
'SELECT id, clave_hash FROM usuarios WHERE nombre = $1',
[nombre]
);
const ok = rows.length > 0 && await bcrypt.compare(clave, rows[0].clave_hash);
Observa que primero se busca solo por nombre y la clave se verifica con bcrypt.compare; nunca se envía la clave a la consulta. La columna del esquema se llama clave_hash:
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL UNIQUE,
clave_hash TEXT NOT NULL,
rol TEXT NOT NULL DEFAULT 'cliente'
);
💡 Consejo · Mensajes de acceso
Ante un fallo responde siempre con el mismo mensaje («usuario o contraseña incorrectos»), para no revelar si el usuario existe.
4.Permisos: el principio del mínimo privilegio
Si tu aplicación se conecta con una cuenta que puede todo, un fallo de seguridad lo pone todo en riesgo. El principio del mínimo privilegio dice que cada cuenta debe tener solo los permisos que necesita. En PostgreSQL los permisos se gestionan con GRANT y REVOKE:
-- La aplicación web puede leer el catálogo y registrar pedidos
CREATE ROLE tienda_app LOGIN PASSWORD 'cambia-esta-clave';
GRANT SELECT ON productos, clientes TO tienda_app;
GRANT SELECT, INSERT ON pedidos, lineas_pedido TO tienda_app;
-- Un analista solo puede leer
CREATE ROLE analista LOGIN PASSWORD 'otra-clave-distinta';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analista;
-- Quitar un permiso
REVOKE INSERT ON pedidos FROM tienda_app;
Con estos permisos, aunque alguien lograra inyectar un DROP TABLE a través de tienda_app, el motor lo rechazaría. SQLite no tiene usuarios ni GRANT: la protección es la del archivo (permisos del sistema operativo) y lo que hace tu programa. Una opción útil es abrir la base en modo de solo lectura cuando el programa no necesita escribir:
const db = new Database('tienda.db', { readonly: true });
5.Copias de seguridad
Los datos se pierden por errores humanos (un DELETE sin WHERE), fallos de disco, virus o ataques. Una copia que nunca has probado restaurar no es una copia. Regla práctica 3-2-1: 3 copias, en 2 tipos de soporte distintos y 1 fuera de tu casa u oficina.
sqlite3 tienda.db ".backup 'copia-2026-10-06.db'"
# Restaurar: copiar el archivo de vuelta y abrirlo
cp copia-2026-10-06.db tienda.db
Usa .backup en lugar de copiar el archivo a mano cuando la base está en uso: copiar mientras alguien escribe puede dejar una copia dañada.
# Copia (formato personalizado, comprimido)
pg_dump -U postgres -F c -f tienda.backup tienda
# Restaurar en una base nueva y vacía
createdb -U postgres tienda_restaurada
pg_restore -U postgres -d tienda_restaurada tienda.backup
Programa las copias para que se hagan solas (con cron en Linux o el Programador de tareas en Windows), cifra las que salgan de tu máquina y comprueba de vez en cuando que se pueden restaurar.
6.Pruébalo: ataque y defensa
Elige un caso de acceso y alterna entre la consulta con texto pegado (vulnerable) y la parametrizada. Los resultados provienen de SQLite real sobre la tabla usuarios de esta lección.
🧪 Ejemplo · Experimentos
1) Con el caso «Entrar como admin sin su clave» y la consulta vulnerable, entras como admin. 2) Cambia a «Parametrizada»: ya no entra nadie. 3) Haz lo mismo con «Entrar como cualquiera»: con texto pegado devuelve a todos los usuarios. 4) Comprueba que el usuario legítimo entra en ambos modos: la defensa no molesta al uso normal.
7.Errores frecuentes
- Construir consultas con
+o con plantillas de texto (`... ${nombre} ...`) usando datos del usuario. - Creer que «escapar comillas a mano» es suficiente: es fácil olvidar un caso. Usa parámetros.
- Guardar contraseñas en texto plano o con un hash rápido sin sal (MD5, SHA-1, SHA-256 a secas).
- Conectar la aplicación con la cuenta de administrador de la base de datos.
- Mostrar el error de la base de datos al usuario: revela nombres de tablas y ayuda al atacante. Regístralo en el servidor y muestra un mensaje genérico.
- Pegar el nombre de una columna elegida por el usuario en
ORDER BYsin validarla con una lista blanca. - No probar nunca la restauración de las copias de seguridad.
- Subir contraseñas o la cadena de conexión a un repositorio público.
📒 Apuntes para llevar
- La inyección SQL aparece al pegar texto del usuario dentro del SQL; el atacante añade sus propias instrucciones.
- Defensa principal: consultas parametrizadas (
?en SQLite,$1en PostgreSQL). Datos e instrucciones viajan por separado. - Los parámetros solo sustituyen valores; para nombres de columnas o de tablas usa una lista blanca.
- Guarda solo el hash de las contraseñas, con bcrypt, scrypt o Argon2 (lentos y con sal).
- Mínimo privilegio: roles con
GRANTyREVOKEpara dar solo lo necesario; en SQLite, modo de solo lectura cuando se pueda. - Copias de seguridad:
.backupen SQLite,pg_dumpypg_restoreen PostgreSQL; regla 3-2-1 y restauración probada. - Credenciales en variables de entorno, errores genéricos para el usuario y detalles solo en el registro del servidor.
8.Glosario
| Término | Significado |
|---|---|
| Inyección SQL | Ataque que introduce instrucciones SQL a través de datos que el programa pega dentro de una consulta. |
| Consulta parametrizada | Consulta con marcadores (?, $1) cuyos valores se envían aparte del texto SQL. |
| Hash | Resultado de una función de un solo sentido; no permite recuperar el texto original. |
| Sal | Valor aleatorio añadido a la clave antes de calcular el hash para que claves iguales den hashes distintos. |
| bcrypt / Argon2 | Algoritmos diseñados para guardar contraseñas: lentos a propósito y con sal. |
| Mínimo privilegio | Principio de dar a cada cuenta solo los permisos imprescindibles. |
| GRANT / REVOKE | Instrucciones de PostgreSQL para dar y quitar permisos a un rol. |
| Lista blanca | Lista de valores permitidos con la que se compara una entrada antes de usarla. |
🧠 Comprueba lo aprendido
Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.
¿Qué causa una inyección SQL?
El usuario puede escribir comillas y SQL que cambian el significado de la consulta.
¿Cuál es la defensa principal contra la inyección SQL?
Con parámetros, el motor trata lo recibido siempre como dato, nunca como instrucciones.
¿Cómo se deben guardar las contraseñas de los usuarios?
Un hash lento y con sal impide recuperar la clave y dificulta los ataques de fuerza bruta.
¿Qué dice el principio del mínimo privilegio?
Así, si una cuenta se ve comprometida, el daño queda limitado.
¿Qué indica la regla de copias 3-2-1?
Varias copias, en soportes distintos y una lejos, protegen contra fallos, robos y desastres.
Tu progreso se guarda en este navegador y se refleja en el índice del curso.
💡 🚀 ¡Ahora te toca!
La práctica fija lo aprendido. Pasa al ejercicio de esta lección.