← Node & Express

APIs REST

¿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."

Reto rápido

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.)