Saltar al contenido
Bases de datos con SQL
Todos los cursos Ir al ejercicio →

Seguridad: inyección SQL, contraseñas y copias

  • ⏱ 60 minutos
  • 📶 Nivel: principiante
  • 🧩 Incluye ejercicio

💡 🎯 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 GRANT y REVOKE.
  • 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.)

Tabla de ejemplo
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):

Código vulnerable (JavaScript)
const sql = "SELECT id, nombre, rol FROM usuarios WHERE nombre = '" + nombre + "' AND clave = '" + clave + "'";

Con un usuario honesto todo parece correcto:

nombre = ana, clave = clave123
SELECT id, nombre, rol FROM usuarios WHERE nombre = 'ana' AND clave = 'clave123'
idnombrerol
1anacliente

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:

nombre = admin' --
SELECT id, nombre, rol FROM usuarios WHERE nombre = 'admin' --' AND clave = 'lo-que-sea'
idnombrerol
2adminadministrador

Entró como administrador sin conocer su clave. Y con ' OR '1'='1 en ambos campos la condición se vuelve siempre verdadera:

nombre y clave = ' OR '1'='1
SELECT id, nombre, rol FROM usuarios WHERE nombre = '' OR '1'='1' AND clave = '' OR '1'='1'
idnombrerol
1anacliente
2adminadministrador

⚠️ 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 --.

Consulta parametrizada
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:

SQLite en Node.js (better-sqlite3)
const fila = db
  .prepare('SELECT id, nombre, rol FROM usuarios WHERE nombre = ? AND clave_hash = ?')
  .get(nombre, clave);
PostgreSQL en Node.js (pg)
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:

SHA-256
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.

Registro y acceso con bcrypt (Node.js)
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:

Esquema correcto
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:

Roles con permisos limitados (PostgreSQL)
-- 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:

Solo lectura en SQLite (better-sqlite3)
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.

SQLite: copiar el archivo de forma segura
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.

PostgreSQL: volcar y restaurar
# 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.

🛡️ Laboratorio de inyección SQL

    

🧪 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

📒 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, $1 en 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 GRANT y REVOKE para dar solo lo necesario; en SQLite, modo de solo lectura cuando se pueda.
  • Copias de seguridad: .backup en SQLite, pg_dump y pg_restore en 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érminoSignificado
Inyección SQLAtaque que introduce instrucciones SQL a través de datos que el programa pega dentro de una consulta.
Consulta parametrizadaConsulta con marcadores (?, $1) cuyos valores se envían aparte del texto SQL.
HashResultado de una función de un solo sentido; no permite recuperar el texto original.
SalValor aleatorio añadido a la clave antes de calcular el hash para que claves iguales den hashes distintos.
bcrypt / Argon2Algoritmos diseñados para guardar contraseñas: lentos a propósito y con sal.
Mínimo privilegioPrincipio de dar a cada cuenta solo los permisos imprescindibles.
GRANT / REVOKEInstrucciones de PostgreSQL para dar y quitar permisos a un rol.
Lista blancaLista 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?

¿Cuál es la defensa principal contra la inyección SQL?

¿Cómo se deben guardar las contraseñas de los usuarios?

¿Qué dice el principio del mínimo privilegio?

¿Qué indica la regla de copias 3-2-1?

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.

Ir al ejercicio