← Ciberseguridad para desarrolladores

Sesión 2

Cinco errores que debes reconocer

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?

Autenticación
  1. Email + contraseña
  2. Usuario Marc

La autorización responde:

¿Qué tienes permiso para hacer?

Autorización
  1. Marc
  2. ¿Puede ver /admin?
  3. 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:

La comprobación que falta
  1. ¿QUIÉN solicita el recurso?
  2. ¿TIENE permiso?
  3. 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
Modificar su perfil
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.

Al registrarse
  1. Contraseña
  2. Función de hash para passwords
  3. Guardamos el resultado, no la contraseña
Al iniciar sesión
  1. Password introducido
  2. Verificación
  3. ¿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:

Cómo un secreto acaba en el historial
  1. Crear .env
  2. Poner la contraseña
  3. git add .
  4. 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:

Todo lo que acaba dentro de nuestra aplicación
  1. Nuestro código
  2. Librerías
  3. Dependencias
  4. Paquetes
  5. Registros
  6. 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=true en producción;
  • CORS: * sin necesidad;
  • un usuario admin con la contraseña por defecto;
  • una base de datos accesible públicamente;
  • puertos abiertos innecesariamente.

Conviene recuperar lo estudiado sobre Azure:

La seguridad está presente en todas las capas
  1. Internet
  2. Firewall
  3. Servidor
  4. 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

  1. Abre app.py y localiza las funciones get_order, search_products, public_config y error_response mediante la búsqueda del editor. Anota qué hace cada una antes de juzgarla.
  2. 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.
  3. 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.
  4. 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.
  5. 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.