Punto de partida. Actividad «Auditoría y correcciones de seguridad», sesión 2 de 5. Abre el avance de la sesión anterior; los pasos de hoy indican qué conservar y qué completar. La guía de arranque permite preparar las herramientas sin depender de otros módulos.
Se explica
10 minutos · contexto, explicación y ejemplo
Los fallos se reconocen por una consecuencia comprobable, no por una palabra marcada por un asistente. Una entrada concatenada a SQL puede cambiar el sentido de la consulta; un identificador de pedido sin comprobación de propietario puede exponer datos ajenos. Un secreto incluido en código y una respuesta de error demasiado detallada crean otros riesgos distintos.
La revisión de contraseñas, dependencias y configuración completa el panorama: las contraseñas reales requieren mecanismos de almacenamiento adecuados y las dependencias deben conocerse y mantenerse. El laboratorio no pretende implementar un sistema de autenticación completo; utilizaremos fichas para distinguir estos conceptos.
Consultar ejemplos y conceptos de esta sesión
1 · Control de acceso
Autenticación y autorización no son lo mismo
Esta diferencia es extremadamente importante.
La autenticación responde:
¿Quién eres?
- Email + contraseña
- Usuario Marc
La autorización responde:
¿Qué tienes permiso para hacer?
- Marc
- ¿Puede ver /admin?
- No
Podemos estar perfectamente autenticados y no estar autorizados para realizar una determinada acción.
Un error muy frecuente
El usuario 15 consulta sus datos:
GET /api/users/15
Ahora modifica la URL:
GET /api/users/16
La aplicación responde con los datos del usuario 16. El login funciona perfectamente, pero existe un grave problema de:
control de acceso
La aplicación debería comprobar siempre:
- ¿QUIÉN solicita el recurso?
- ¿TIENE permiso?
- Entregar información
No basta con que exista una sesión válida.
Principio de mínimo privilegio
Un usuario o aplicación debería tener solo los permisos que necesita para realizar su trabajo.
| Acción de un usuario normal | ¿Debería poder? |
|---|---|
| Leer sus pedidos | Sí |
| Modificar su perfil | Sí |
| Borrar otros usuarios | No |
| Gestionar administradores | No |
Una base de datos utilizada únicamente para leer informes quizá no necesita permisos para ejecutar DROP TABLE.
Menos permisos significa menos daño posible si algo sale mal.
2 · Entradas e inyección
Nunca confíes completamente en los datos que llegan
Supongamos:
const username = req.query.username;
El usuario controla ese valor. Puede enviar Marc, pero también cualquier otra cosa.
Por tanto, cualquier entrada externa debe considerarse potencialmente no confiable. Puede llegar desde formularios, la URL, cookies, una API, un fichero, las cabeceras u otra aplicación.
Inyección
Observad:
const query =
"SELECT * FROM users WHERE username = '" +
username +
"'";
El resultado es aparentemente correcto, pero la consulta se construye concatenando código SQL con entrada del usuario, lo que puede permitir una vulnerabilidad de:
SQL Injection
La solución general
No debemos construir consultas concatenando directamente datos externos. Utilizamos consultas parametrizadas, prepared statements o un ORM correctamente utilizado.
Mal
SQL y datos mezclados en la misma cadena.
Bien
El SQL define la estructura; los datos viajan aparte como valores.
Otro tipo de inyección: XSS
Imaginad que un usuario escribe un comentario y lo mostramos en nuestra web. Si en lugar de texto introduce contenido que el navegador interpreta como código, y lo insertamos sin las protecciones adecuadas, podemos provocar:
Cross-Site Scripting — XSS
La idea importante vuelve a ser la misma: los datos externos no son automáticamente seguros.
Los frameworks modernos proporcionan muchas protecciones. No debemos desactivarlas sin entender las consecuencias.
3 · Contraseñas y secretos
Las contraseñas
Observad esta tabla:
| usuario | password |
|---|---|
| ana | patata123 |
| pepe | qwerty |
¿Está bien almacenar contraseñas así?
No
Una contraseña no debería almacenarse en texto plano.
Hash de contraseñas
Normalmente almacenamos un resultado derivado mediante una función apropiada para contraseñas.
- Contraseña
- Función de hash para passwords
- Guardamos el resultado, no la contraseña
- Password introducido
- Verificación
- ¿Coincide?
Para passwords se utilizan algoritmos específicamente diseñados para ello: Argon2, bcrypt, scrypt o PBKDF2. No inventamos nuestro propio sistema criptográfico.
Cifrado y funciones hash: dos operaciones distintas
No.
Cifrado
Queremos poder recuperar la información original utilizando una clave.
Hash de contraseña
No necesitamos recuperar la contraseña, solo comprobar después si la introducida es correcta.
Secretos
Nunca deberíamos encontrar esto en el repositorio:
const API_KEY = "sk-123456789";
const DB_PASSWORD = "admin123";
Mucho menos si después hacemos git push a GitHub.
Una posibilidad habitual para guardarlos son:
variables de entorno
Por ejemplo DB_PASSWORD, API_KEY o JWT_SECRET. La aplicación obtiene el valor del entorno y el secreto no queda almacenado en el código.
Y cuidado con .env
Un fichero .env puede contener secretos, por lo que normalmente debe aparecer en .gitignore.
El error clásico es este:
- Crear
.env - Poner la contraseña
git add .git push
El secreto permanece en el historial del repositorio aunque se elimine posteriormente del código.
4 · Dependencias y configuración
El problema de las dependencias
Nuestro programa puede tener 500 líneas propias y depender de 50.000 o 500.000 líneas escritas por terceros:
"dependencies": {
"express": "...",
"axios": "...",
"jsonwebtoken": "..."
}
Cada dependencia añade código, mantenimiento, posibles vulnerabilidades y riesgo de cadena de suministro.
Cadena de suministro de software
Nuestra aplicación no está formada únicamente por nuestro código:
- Nuestro código
- Librerías
- Dependencias
- Paquetes
- Registros
- Herramientas de build
Si cualquiera de estas piezas está comprometida, nuestro software también puede estarlo. Por eso debemos evitar dependencias innecesarias, mantenerlas actualizadas, revisar alertas, utilizar fuentes conocidas y entender qué instalamos.
Herramientas como Dependabot pueden avisarnos de dependencias vulnerables, versiones antiguas y actualizaciones disponibles. La seguridad no depende solo de revisar código a mano: también podemos usar automatización.
Configuración insegura
Una aplicación puede tener código correcto y estar mal configurada. Por ejemplo:
DEBUG=trueen producción;CORS: *sin necesidad;- un usuario
admincon la contraseña por defecto; - una base de datos accesible públicamente;
- puertos abiertos innecesariamente.
Conviene recuperar lo estudiado sobre Azure:
- Internet
- Firewall
- Servidor
- Aplicación
5 · Errores y logs
Los errores también pueden revelar información
Imaginad que nuestra aplicación responde esto a un usuario cualquiera:
Error SQL: password authentication failed for user postgres
Database: 10.0.0.12
Path: /home/app/backend/database.js
Acabamos de regalar el motor de base de datos, una dirección interna y la estructura del proyecto. Un usuario debería recibir algo parecido a «Se ha producido un error», y los detalles quedar registrados internamente.
Logging
Ocultar los errores al usuario no significa no registrarlos. Necesitamos saber qué ocurrió, cuándo, dónde y qué usuario estaba implicado.
Usuario
Un mensaje sencillo, sin detalles internos.
Servidor
Un log con la información técnica necesaria para investigar.
Tampoco procede almacenar sin criterio contraseñas, tokens, números de tarjeta ni secretos.
HTTPS
Ya lo utilizamos en Azure. HTTP no proporciona por sí mismo protección TLS; con HTTPS obtenemos confidencialidad, integridad y autenticación del servidor mediante certificado.
Pero HTTPS no convierte automáticamente una aplicación insegura en segura: una web con SQL Injection sigue siendo vulnerable aunque utilice HTTPS.
Se trabaja
45 minutos · trabajo guiado sobre la actividad
- Abre
app.pyy localiza las funcionesget_order,search_products,public_configyerror_responsemediante la búsqueda del editor. Anota qué hace cada una antes de juzgarla. - Sigue el caso resuelto de pedido ajeno del guia-seguridad.pdf. Compara el resultado con tu matriz: saber que el pedido existe no concede permiso para leerlo.
- Ejecuta
python -m unittest -v. Las pruebas de seguridad fallan en la versión inicial de forma deliberada. Lee una aserción y tradúcela a una regla del negocio. - Revisa la ficha de contraseñas, secretos, dependencias y logs. Clasifica cada ejemplo según dato expuesto, posible daño y medida de prevención; no copies un algoritmo de cifrado como solución universal.
- Corrige el primer fallo de propietario siguiendo las pistas del guia-seguridad.pdf y ejecuta su prueba. Guarda la evidencia inicial y la posterior; no declares corregidas las otras familias porque una prueba pase.
Cierre
5 minutos · comprobar el resultado
Al terminar la sesión:
Se ha explicado y verificado una corrección. La tabla distingue autorización, entrada SQL, configuración y respuesta de error, además de los conceptos de las fichas.