¿Qué medidas de seguridad básicas aplicarías en una API REST con Express?
Ver respuesta — intenta responderla en voz alta primero
Como mínimo: usar helmet para cabeceras HTTP seguras, configurar cors para controlar qué orígenes pueden llamar a la API, no exponer stack traces en producción, aplicar rate limiting contra abuso y fuerza bruta, prevenir inyección con consultas parametrizadas y validación de input, y no hardcodear secretos, sino leerlos de variables de entorno.
Una API pública recibe peticiones de cualquiera, así que hay que protegerla en varias capas:
- helmet: cabeceras seguras. Es un middleware que añade cabeceras HTTP que endurecen la seguridad (por ejemplo, evitan que la respuesta se interprete como un tipo distinto o que se cargue en un iframe ajeno). Se activa con una línea y ya mejora bastante.
- cors: control de orígenes. CORS decide qué dominios del navegador pueden llamar a tu API. Sin configurarlo bien, cualquier web podría consumir tu API desde el navegador. Se restringe a los orígenes que confías.
- No exponer stack traces en producción. Si devuelves el error interno completo al cliente, revelas rutas de archivos, versiones y detalles del sistema que ayudan a un atacante. En producción respondes un mensaje genérico y registras el detalle solo en tus logs.
- Rate limiting. Limitar cuántas peticiones puede hacer una IP en un periodo. Protege contra abuso y contra fuerza bruta (por ejemplo, probar miles de contraseñas en el login). Se hace con express-rate-limit.
- Prevención de inyección. Nunca concatenes input del usuario dentro de una consulta. Usa consultas parametrizadas (placeholders) y valida el input. Así el dato del usuario se trata como dato, no como código.
- No hardcodear secretos. Claves de API, contraseñas de base de datos y tokens no van en el código. Van en variables de entorno (
process.env), fuera del control de versiones.
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(express.json());
// 1) Cabeceras HTTP seguras (una línea, gran mejora)
app.use(helmet());
// 2) CORS: solo permitimos el origen de nuestro frontend
app.use(
cors({
origin: 'https://mi-frontend.com', // no usar '*' en producción con credenciales
})
);
// 3) Rate limiting: máximo 100 peticiones por IP cada 15 minutos
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // máximo de peticiones por IP en esa ventana
message: { error: 'Demasiadas peticiones, inténtalo más tarde' },
});
app.use(limiter);
// 4) Secretos desde variables de entorno, NUNCA en el código
const apiKey = process.env.API_KEY; // se define fuera del código fuente
app.get('/salud', (req, res) => {
res.json({ estado: 'ok' });
});
// 5) Manejador de errores central: NO exponer stack traces en producción
app.use((err, req, res, next) => {
// El detalle solo va a los logs del servidor
console.error(err);
const enProduccion = process.env.NODE_ENV === 'production';
res.status(500).json({
error: 'Error interno del servidor',
// Solo mostramos detalles fuera de producción
detalle: enProduccion ? undefined : err.message,
});
});
app.listen(3000);
Ejemplo de consulta parametrizada para prevenir inyección SQL (usando un cliente de PostgreSQL como pg):
// MAL: concatenar input del usuario permite inyección SQL
// const query = `SELECT * FROM usuarios WHERE email = '${email}'`;
// BIEN: el email viaja como parámetro ($1), nunca como parte del texto SQL
const resultado = await db.query(
'SELECT * FROM usuarios WHERE email = $1',
[email]
);
# Los secretos se pasan por entorno, no se escriben en el código
NODE_ENV=production API_KEY=mi-clave-secreta node app.js
Hardcodear secretos en el código (una API key o la contraseña de la base de datos escritas directamente en un archivo .js) y subirlos al repositorio. Una vez en el historial de Git, quedan expuestos aunque los borres después. Los secretos van en variables de entorno y el archivo que los contiene (por ejemplo .env) se ignora en el control de versiones.
Otro error frecuente es devolver el stack trace completo al cliente en producción, revelando estructura interna y versiones que facilitan un ataque.
"Empezaría por lo básico que da mucho valor con poco esfuerzo: helmet para añadir cabeceras HTTP seguras y cors bien configurado para permitir solo los orígenes que confío, no un asterisco abierto. Después me aseguro de no exponer stack traces en producción; devuelvo un mensaje genérico al cliente y dejo el detalle solo en los logs. Añado rate limiting con express-rate-limit para frenar abuso y ataques de fuerza bruta, sobre todo en endpoints como el login. Para inyección, uso siempre consultas parametrizadas y valido el input, de forma que el dato del usuario nunca se interpreta como código. Y por supuesto no hardcodeo secretos: van en variables de entorno y fuera del control de versiones."
Un compañero configuró CORS así en producción y además dejó la clave en el código:
app.use(cors({ origin: '*' }));
const dbPassword = 'superSecreta123';
Menciona dos problemas de seguridad y cómo los corregirías.
Ver respuesta
Problema 1: origin: '*' permite que cualquier web llame a la API desde el navegador. En producción hay que restringirlo a los orígenes de confianza:
app.use(cors({ origin: 'https://mi-frontend.com' }));
Problema 2: la contraseña está hardcodeada en el código. Cualquiera con acceso al repositorio la ve, y queda en el historial de Git. Debe leerse de una variable de entorno:
const dbPassword = process.env.DB_PASSWORD;
(El valor real se define fuera del código, por ejemplo en un archivo .env ignorado por Git o en la configuración del servidor.)