10 Patrones de Seguridad en Python — Guía Práctica con Código para Herramientas Web Seguras

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ónPropósitoPrioridadOWASP
① Generación segura de aleatoriosTokens impredeciblesObligatorioA02 Fallos criptográficos
② Almacenamiento de contraseñasProtección de credencialesObligatorioA02 Fallos criptográficos
③ Diseño seguro de JWTControl de autenticaciónObligatorioA07 Fallos de autenticación
④ Prevención de SQL InjectionProtección de base de datosObligatorioA03 Inyección
⑤ Prevención de XSSSeguridad de visualizaciónObligatorioA03 Inyección
⑥ Protección CSRFSeguridad de formulariosObligatorioA01 Control de acceso roto
⑦ Limitación de tasaPrevención de abusoImportante
⑧ Diseño de logsPrevención de incidentesImportanteA09 Fallos de registro
⑨ Gestión de secretosPrevención de filtracionesObligatorioA02 Fallos criptográficos
⑩ Configuración por entornoSeguridad operativaObligatorioA05 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:

NG
import random
token = random.randint(100000, 999999)  # ¡Predecible!

Ejemplo correcto:

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

💡 Tip

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:

NG
password = "user_password"
db.save(password)  # Texto plano → brecha instantánea

Ejemplo correcto:

OK
import bcrypt

password = b"mypassword"
hashed = bcrypt.hashpw(password, bcrypt.gensalt())

# Verificar
is_valid = bcrypt.checkpw(password, hashed)
print(is_valid)  # True
Bash
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.

⚠️ Error común

«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:

NG
import jwt
token = jwt.encode({"user_id": 1}, "secret")  # Sin expiración, secret débil

Ejemplo correcto:

OK
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"])
Bash
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
💡 Tip

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:

NG
# 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:

OK
# 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.

⚠️ Error común

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:

NG
# Devolviendo entrada del usuario tal cual
return user_input
# Si el atacante ingresa <script>alert(1)</script> se ejecuta

Ejemplo correcto:

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

💡 Tip

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

Django Template
<form method="POST">
    {% csrf_token %}
    <input type="text" name="data">
    <button type="submit">Enviar</button>
</form>

FastAPI:

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

FastAPI + slowapi
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"}
Bash
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
💡 Tip

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:

NG
# Nunca hagas esto
print(f"Login: user={username}, password={password}")
logging.info(f"Token: {api_token}")

Ejemplo correcto:

OK
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)
⚠️ Error común

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:

NG
# Hardcodeado → filtración instantánea al hacer push
SECRET_KEY = "abc123superSecret"
DB_PASSWORD = "production_password"

Ejemplo correcto:

OK
import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.getenv("SECRET_KEY")
DB_PASSWORD = os.getenv("DB_PASSWORD")
Bash
pip install python-dotenv

Archivo .env de ejemplo:

.env
SECRET_KEY=9f3c0c6d61c3c2e7b2c5a2b41a6f9d88
DB_PASSWORD=strong_random_password_here

Y siempre agrega .env a tu .gitignore.

💡 Tip

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.

settings.py
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 secrets para 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ónIncidenteContramedida
#1Secretos subidos a Git.env + .gitignore
#2DEBUG activo en producciónConfiguración por entorno
#3Concatenación de strings SQLParameter binding
#4JWT sin expiraciónConfiguración obligatoria de exp
#5Sin límite de tasaslowapi / 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:

  1. Generar valores aleatorios seguros con secrets
  2. Hashear contraseñas con bcrypt
  3. Gestionar secretos via variables de entorno
  4. Usar parameter binding para seguridad SQL
  5. 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.

Comments

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *