Al desarrollar herramientas web y APIs con Python, la mayoría de los desarrolladores se centran en la implementación de funcionalidades. Pero en proyectos reales, la calidad del diseño de seguridad equivale a la calidad del servicio. Un servicio sin protección de datos, resistencia a ataques ni seguridad operativa nunca generará confianza, sin importar cuán ricas sean sus funcionalidades.
Este artículo cubre 10 patrones de seguridad que todo desarrollador Python debería implementar como mínimo, con ejemplos de código NG (malo) y OK (bueno). Ya sea que uses Django, FastAPI o escribas scripts Python, esta guía es para ti.
Para conocimientos fundamentales sobre contraseñas, hashes y tokens, consulta «Contraseñas, UUIDs, Hashes y Tokens: Guía Completa.»
Resumen: 10 Patrones de Seguridad en Python
Comencemos con una visión general. Aquí están los 10 patrones de un vistazo, con enlaces a cada sección.
| Patrón | Propósito | Prioridad | OWASP |
|---|---|---|---|
| ① Generación segura de aleatorios | Tokens impredecibles | Obligatorio | A02 Fallos criptográficos |
| ② Almacenamiento de contraseñas | Protección de credenciales | Obligatorio | A02 Fallos criptográficos |
| ③ Diseño seguro de JWT | Control de autenticación | Obligatorio | A07 Fallos de autenticación |
| ④ Prevención de SQL Injection | Protección de base de datos | Obligatorio | A03 Inyección |
| ⑤ Prevención de XSS | Seguridad de visualización | Obligatorio | A03 Inyección |
| ⑥ Protección CSRF | Seguridad de formularios | Obligatorio | A01 Control de acceso roto |
| ⑦ Limitación de tasa | Prevención de abuso | Importante | — |
| ⑧ Diseño de logs | Prevención de incidentes | Importante | A09 Fallos de registro |
| ⑨ Gestión de secretos | Prevención de filtraciones | Obligatorio | A02 Fallos criptográficos |
| ⑩ Configuración por entorno | Seguridad operativa | Obligatorio | A05 Mala configuración |
También incluimos las correspondencias con OWASP Top 10. Cubrir estos 10 patrones por sí solo aborda la mayoría de los riesgos identificados por OWASP.
① Generación Segura de Aleatorios — Nunca Uses random
Al generar tokens, códigos de autenticación o URLs de restablecimiento de contraseña, el módulo random de Python nunca debe usarse. random usa el algoritmo Mersenne Twister, que es predecible: un atacante que observe unos cientos de salidas puede predecir valores futuros.
Ejemplo malo:
import random
token = random.randint(100000, 999999) # ¡Predecible!
Ejemplo correcto:
import secrets
token = secrets.token_hex(16)
print(token)
# Ejemplo: 9f3c0c6d61c3c2e7b2c5a2b41a6f9d88
El módulo secrets utiliza el generador de números pseudoaleatorios criptográficamente seguro (CSPRNG) del sistema operativo, haciendo la predicción prácticamente imposible. Usa secrets para toda aleatoriedad relacionada con seguridad.
secrets.token_urlsafe(32) genera tokens seguros para URLs, perfectos para enlaces de restablecimiento de contraseña. Consulta «Patrones de Generación de Contraseñas en Python» para más detalles.
② Almacenamiento de Contraseñas — Texto Plano Es Inaceptable
Almacenar contraseñas en texto plano es uno de los fallos de seguridad más catastróficos. En el momento en que tu base de datos se filtra, todas las contraseñas de usuarios caen en manos del atacante.
Ejemplo malo:
password = "user_password"
db.save(password) # Texto plano → brecha instantánea
Ejemplo correcto:
import bcrypt
password = b"mypassword"
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
# Verificar
is_valid = bcrypt.checkpw(password, hashed)
print(is_valid) # True
pip install bcrypt
Usar funciones hash de propósito general como SHA-256 solas también es peligroso. SHA-256 es demasiado rápido: los atacantes pueden probar miles de millones de hashes por segundo. bcrypt y Argon2 son intencionalmente lentos, haciendo los ataques de fuerza bruta exponencialmente más costosos.
«Hice hash con SHA-256, así que es seguro» es incorrecto. Siempre usa algoritmos intencionalmente lentos como bcrypt, Argon2 o scrypt para almacenar contraseñas. Para fundamentos de hashing, consulta «Contraseñas, UUIDs, Hashes y Tokens.»
③ Diseño Seguro de JWT
JWT (JSON Web Token) se usa ampliamente para autenticación sin estado, pero una mala configuración genera vulnerabilidades graves.
Ejemplo malo:
import jwt
token = jwt.encode({"user_id": 1}, "secret") # Sin expiración, secret débil
Ejemplo correcto:
import jwt
import datetime
payload = {
"user_id": 1,
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
token = jwt.encode(payload, "STRONG_SECRET_KEY_HERE", algorithm="HS256")
# Verificar
data = jwt.decode(token, "STRONG_SECRET_KEY_HERE", algorithms=["HS256"])
pip install PyJWT
Tres reglas para un diseño JWT seguro:
- Siempre establecer expiración (exp) — tokens sin expiración pueden explotarse para siempre si se filtran
- Especificar el algoritmo explícitamente — omitirlo arriesga ataques de algoritmo «none»
- Usar una clave secreta fuerte — generar con
secrets.token_hex(32)y almacenar en variables de entorno
La expiración del access token debe ser de 15 minutos a 1 hora. Para sesiones persistentes, combina con refresh tokens y mantén los access tokens de vida corta.
④ Prevención de SQL Injection
La inyección SQL es una de las vulnerabilidades más antiguas y aún más comunes. Concatenar entrada del usuario directamente en sentencias SQL permite a los atacantes manipular tu base de datos a voluntad.
Ejemplo malo:
# Nunca hagas esto
query = "SELECT * FROM users WHERE name='" + name + "'"
cursor.execute(query)
Si name contiene admin' OR '1'='1, todos los datos de usuarios quedan expuestos.
Ejemplo correcto:
# Usar parameter binding
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
Con parameter binding, la entrada del usuario se trata como datos, no como parte de la sentencia SQL. Django ORM y SQLAlchemy usan este enfoque por defecto, pero siempre usa parameter binding al escribir SQL crudo.
Los f-strings (f"SELECT ... WHERE name='{name}'") son igual de peligrosos que la concatenación. Pueden verse más limpios, pero el riesgo de inyección SQL es idéntico.
⑤ Prevención de XSS
XSS (Cross-Site Scripting) ocurre cuando la entrada del usuario se renderiza directamente en HTML, permitiendo la ejecución de scripts maliciosos en el navegador.
Ejemplo malo:
# Devolviendo entrada del usuario tal cual
return user_input
# Si el atacante ingresa <script>alert(1)</script> se ejecuta
Ejemplo correcto:
import html
safe_output = html.escape(user_input)
return safe_output
# <script> → <script> — neutralizado
El principio de prevención de XSS es «nunca confíes en la salida». Escapa en la etapa de salida (al renderizar HTML), no en la entrada. Las plantillas de Django y Jinja2 habilitan el escape HTML por defecto, pero ten especial cuidado con los filtros |safe o mark_safe().
Al usar {{ variable|safe }} en plantillas Django, aplícalo solo a datos confiables. Aplicar |safe a entrada del usuario es abrir la puerta al XSS.
⑥ Protección CSRF
CSRF (Cross-Site Request Forgery) engaña a usuarios autenticados para enviar solicitudes no deseadas. La defensa es incluir tokens emitidos por el servidor en formularios y validarlos al enviar.
Django (integrado):
<form method="POST">
{% csrf_token %}
<input type="text" name="data">
<button type="submit">Enviar</button>
</form>
FastAPI:
from itsdangerous import URLSafeSerializer
s = URLSafeSerializer("secret-key")
csrf_token = s.dumps("session_id")
# Verificar
try:
data = s.loads(received_token)
except Exception:
raise HTTPException(status_code=403)
pip install itsdangerous
Django tiene protección CSRF habilitada por defecto. Para frameworks sin protección CSRF integrada como FastAPI, necesitarás implementar la generación y validación de tokens.
⑦ Limitación de Tasa (Rate Limiting)
La limitación de tasa restringe el número de solicitudes en una ventana de tiempo. Previene ataques de fuerza bruta en login, abuso de API y flooding de bots.
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.get("/api/data")
@limiter.limit("10/minute")
def get_data():
return {"status": "ok"}
pip install slowapi
Para Django, django-ratelimit es la opción estándar. Siempre establece límites de tasa en estos endpoints:
- Login — objetivo directo de ataques de fuerza bruta
- Restablecimiento de contraseña — previene bombardeo de emails
- Endpoints de API — previene abuso y escalada de costos
- Formularios de registro — previene creación de cuentas spam
La limitación de tasa no «previene completamente» los ataques — aumenta dramáticamente el costo de atacar. Combinarla con limitación de tasa a nivel Nginx bloquea solicitudes antes de llegar a tu aplicación.
⑧ Diseño de Logs — Lo Que Nunca Debes Registrar
El logging es esencial para depuración y monitoreo de seguridad, pero hay información que nunca debe aparecer en los logs. Si contraseñas, tokens, API keys o session IDs terminan en archivos de log, esos archivos se convierten en vectores de ataque.
Ejemplo malo:
# Nunca hagas esto
print(f"Login: user={username}, password={password}")
logging.info(f"Token: {api_token}")
Ejemplo correcto:
import logging
logging.info("Login attempt: user=%s, ip=%s", username, request_ip)
logging.warning("Failed login: user=%s, ip=%s", username, request_ip)
Las reglas son simples:
- Nunca registrar: contraseñas, tokens, API keys, cookies, datos personales
- Sí registrar: IDs de usuario, direcciones IP, nombres de acción, timestamps, resultados (éxito/fallo)
Desplegar con DEBUG=True en producción expone stack traces, variables de entorno y detalles de conexión a la base de datos. En Django, siempre configura DEBUG=False en producción.
⑨ Gestión de Secretos
API keys, contraseñas de bases de datos, claves secretas JWT — estos secretos nunca deben estar hardcodeados en el código fuente. En el momento que tu código aparece en GitHub, todo se filtra.
Ejemplo malo:
# Hardcodeado → filtración instantánea al hacer push
SECRET_KEY = "abc123superSecret"
DB_PASSWORD = "production_password"
Ejemplo correcto:
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv("SECRET_KEY")
DB_PASSWORD = os.getenv("DB_PASSWORD")
pip install python-dotenv
Archivo .env de ejemplo:
SECRET_KEY=9f3c0c6d61c3c2e7b2c5a2b41a6f9d88
DB_PASSWORD=strong_random_password_here
Y siempre agrega .env a tu .gitignore.
GitHub tiene «Secret Scanning» que detecta automáticamente API keys comprometidas. Sin embargo, los atacantes pueden ser más rápidos. La mejor defensa es nunca hacer commit de secretos.
⑩ Configuración por Entorno
Usar la misma configuración en desarrollo, staging y producción es una invitación a accidentes. Modo debug activo en producción, conexiones a bases de datos de prueba manipulando datos en vivo — estos son incidentes reales.
import os
ENV = os.getenv("ENV", "development")
if ENV == "production":
DEBUG = False
ALLOWED_HOSTS = ["example.com"]
else:
DEBUG = True
ALLOWED_HOSTS = ["*"]
Separar la configuración mediante variables de entorno permite cambiar el comportamiento por entorno sin modificar código. Para Django, django-environ agrega gestión con tipos seguros; para FastAPI, pydantic-settings ofrece beneficios similares.
Checklist de Seguridad e Incidentes Comunes
Revisa esta lista antes de cada lanzamiento:
- □ ¿Usas
secretspara generación aleatoria? - □ ¿Contraseñas hasheadas con bcrypt/Argon2?
- □ ¿JWT tiene expiración (exp)?
- □ ¿SQL usa parameter binding?
- □ ¿Salida HTML escapada?
- □ ¿Formularios incluyen tokens CSRF?
- □ ¿Login/API tiene límite de tasa?
- □ ¿Logs libres de contraseñas/tokens?
- □ ¿Secretos gestionados via variables de entorno?
- □ ¿DEBUG=False en producción?
También conoce los incidentes de seguridad más comunes:
| Posición | Incidente | Contramedida |
|---|---|---|
| #1 | Secretos subidos a Git | .env + .gitignore |
| #2 | DEBUG activo en producción | Configuración por entorno |
| #3 | Concatenación de strings SQL | Parameter binding |
| #4 | JWT sin expiración | Configuración obligatoria de exp |
| #5 | Sin límite de tasa | slowapi / django-ratelimit |
Prevenir solo estos cinco cubre la gran mayoría de incidentes de seguridad encontrados en la práctica.
Preguntas Frecuentes
P: ¿Cuál es el mínimo para seguridad en Python?
secrets (aleatorios seguros), bcrypt (almacenamiento de contraseñas), variables de entorno (gestión de secretos), parameter binding (seguridad SQL) y html.escape (prevención XSS). Estos cinco solos crean una enorme diferencia frente a aplicaciones sin seguridad.
P: ¿Django es seguro por defecto?
Los valores predeterminados de Django son sólidos — tokens CSRF, escape XSS y prevención de inyección SQL están integrados. Sin embargo, la mala configuración es común: dejar DEBUG=True en producción, mal uso de filtros |safe y SQL crudo descuidado son fuentes frecuentes de vulnerabilidades.
P: ¿JWT siempre es necesario?
No. Para aplicaciones web pequeñas, la autenticación basada en sesiones funciona bien. JWT brilla en arquitecturas de microservicios o SPAs donde se necesita autenticación sin estado.
P: ¿Es necesaria la limitación de tasa?
Para cualquier API o formulario de login accesible públicamente, sí. Puede no ser necesaria para scripts internos, pero endpoints públicos sin límite de tasa son objetivos fáciles para bots y ataques de fuerza bruta.
P: ¿Cuál es el incidente de seguridad más común?
Secretos (API keys, contraseñas) subidos a GitHub. Bots escanean GitHub constantemente buscando API keys, y la explotación puede ocurrir minutos después de la exposición.
Conclusión
Construir herramientas web seguras en Python no requiere criptografía avanzada — requiere dominar los fundamentos. Entre los 10 patrones cubiertos, estos son los 5 principales:
- Generar valores aleatorios seguros con
secrets - Hashear contraseñas con bcrypt
- Gestionar secretos via variables de entorno
- Usar parameter binding para seguridad SQL
- Prevenir XSS con
html.escape
La seguridad no es algo que se agrega después — es algo que se diseña desde el inicio. Este principio es la base del desarrollo profesional en Python.

Deja una respuesta