---
name: checklist-seguridad
description: Checklist de seguridad basado en OWASP Top 10 + seguridad de agentes autónomos (A11). Cubre inyección, autenticación, exposición de datos, control de acceso, configuración insegura, XSS, deserialización, componentes vulnerables, logging y agencia excesiva de IA. Produce reporte con hallazgos y remediaciones.
version: "1.1.1"
evolved: true
evolved-from: "1.1.0"
evolved-at: "2026-05-04"
evolved-by: "aprender"
evolved-note: "Corregir contradicción interna en ejemplo BIEN de IDOR pre-side-effect: el detail exponía UUID interno violando el gotcha 'vocabulario interno' de fastapi-experto v1.1.1. Detail al cliente ahora es genérico (Recurso no encontrado); UUID y tenant solo en logger.info para auditoría."
herramientasPermitidas: [Read, Grep]
evolvable: true  # default para skill estandar
nist_csf: [PR.PS-01, PR.DS-02, PR.DS-10, DE.CM-09, RS.MI-01]
attack_techniques: [T1190, T1059, T1556]
exclusiones:
  - "No cargar para auditoría de dependencias (CVEs en paquetes npm/pip, licencias) — para dependencias cargar `dependencias-auditoria`."
  - "No cargar para revisión de calidad general (cobertura, DRY, arquitectura) — para calidad general cargar `checklist-calidad`."
  - "No cargar para configuración de certificados TLS, WAF o firewalls de red — este skill es revisión de código, no de infraestructura de red."
  - "No cargar para penetration testing o red teaming — este skill es análisis estático de código, no explotación activa."
---
# Habilidad: Checklist de Seguridad OWASP Top 10

## Cuándo NO cargar

- La auditoría es de CVEs en paquetes pip/npm o licencias: cargar `dependencias-auditoria`.
- La revisión es de calidad general (cobertura, DRY, conformidad con spec): cargar `checklist-calidad`.
- La tarea es configurar TLS, WAF o reglas de firewall: este skill es análisis de código, no de infraestructura de red.
- La tarea es penetration testing o explotación activa: este skill cubre análisis estático, no red teaming.

## Propósito

La seguridad no es una fase final — es una revisión que ocurre en cada merge
relevante. Esta habilidad aplica el OWASP Top 10 de forma estructurada al
código del proyecto, identificando vulnerabilidades concretas con evidencia
y proponiendo remediaciones específicas (no genéricas).

## Cuándo activar

- Antes de hacer deploy a producción
- Al revisar código que maneja autenticación, datos sensibles o inputs de usuario
- Como parte del proceso de revisión de PRs en módulos críticos
- Después de añadir integraciones con sistemas externos

---

## A01 — Broken Access Control

**Verificar**:
- [ ] Todos los endpoints con datos sensibles validan el rol del usuario
- [ ] Los filtros de datos usan el ID del JWT, no el ID del query param
- [ ] No existe escalación de privilegios: usuario normal no puede acceder a rutas de admin
- [ ] Las rutas de administración tienen middleware de autorización diferenciado
- [ ] Los recursos de un usuario no son accesibles con el ID de otro usuario (IDOR)

**Búsquedas de código**:
```bash
# Endpoints sin autenticación (FastAPI)
grep -rn "def [a-z_]*(" --include="*.py" -A3 | grep -v "Depends\|current_user" | head -30

# Parámetros de usuario en query sin validación de JWT
grep -rn "user_id.*Query\|owner_id.*Query" --include="*.py" | head -20
```

**Señal de vulnerabilidad**: endpoint que filtra por `user_id` recibido como
parámetro de URL sin compararlo con el `user_id` del token JWT.

### IDOR pre-side-effect: validar pertenencia ANTES de operaciones costosas

Un endpoint que sube/cifra/procesa material antes de validar que el recurso pertenece al
tenant del usuario es un IDOR amplificado: aunque RLS o el FK rechacen la operación al
final, los **side-effects intermedios** (lectura de bytes del cliente, cifrado AES-GCM,
upload a MinIO/S3 bajo un namespace, llamada costosa a API externa) **ya ocurrieron** y
pueden contaminar storage, agotar recursos o filtrar PII bajo el namespace incorrecto.

```python
# MAL — IDOR pre-side-effect: el upload ocurre antes del check RLS
async def subir_evidencia(entrega_id: UUID, archivo: UploadFile, ...):
    contenido = await archivo.read()                    # side-effect: bytes leídos
    cifrado = aes_gcm_encrypt(contenido, master_key)    # side-effect: PII cifrada
    object_key = f"tenant_{usuario.tenant_id}/{entrega_id}/{uuid4()}"
    await minio.put_object(object_key, cifrado)         # side-effect: subido a MinIO
    # AHORA el INSERT corre con RLS — si entrega_id es de otro tenant, falla con 404,
    # pero el PII ya quedó cifrado bajo el namespace de tenant_A para entrega_B.
    await repo.insert_evidencia(entrega_id, object_key, ...)
```

```python
# BIEN — validar pertenencia ANTES de cualquier side-effect costoso
async def subir_evidencia(entrega_id: UUID, archivo: UploadFile, ...):
    if await repo_entrega.obtener_por_id(entrega_id) is None:
        # RLS filtra el SELECT — None significa "no existe en este tenant".
        # Detail genérico al cliente; UUID + tenant solo en log interno.
        # (Ver gotcha "vocabulario interno en detail" de fastapi-experto.)
        logger.info(
            "IDOR check fallido: recurso %s no accesible para tenant %s",
            entrega_id, usuario.tenant_id,
        )
        raise HTTPException(404, "Recurso no encontrado.")

    contenido = await archivo.read()                    # solo si validación pasó
    cifrado = aes_gcm_encrypt(contenido, master_key)
    ...
```

**Cuándo aplicar**: cualquier endpoint que combine path-param de recurso (`{recurso_id}`)
con uno o más de estos side-effects: `archivo.read()`, cifrado, upload a storage externo,
llamada a API costosa de PSP/proveedor, generación de PDF, envío de email/SMS. Antes
del primer side-effect, ejecutar `repo.obtener_por_id(recurso_id)` y retornar 404 si None.

**Mecanismos que NO son suficientes por sí solos**:
- RLS DENY-by-default: protege el INSERT/UPDATE/SELECT, pero NO los side-effects intermedios.
- FK constraint: protege la integridad referencial, pero el side-effect ya ocurrió.
- `require_permiso`: valida que el usuario tiene permiso de la acción, NO que el recurso
  específico pertenezca a su tenant.

La validación pre-side-effect es **defensa en profundidad**: complementa RLS/FK, no las reemplaza.

---

## A02 — Cryptographic Failures (Exposición de datos sensibles)

**Verificar**:
- [ ] Contraseñas almacenadas con bcrypt/argon2 (NUNCA MD5, SHA1, SHA256 simple)
- [ ] Datos sensibles en tránsito solo sobre HTTPS/TLS
- [ ] Tokens JWT con expiración corta y firmados con clave segura
- [ ] Claves API, secretos y contraseñas SOLO en variables de entorno o vault
- [ ] No hay datos sensibles en logs (tarjetas, contraseñas, tokens)
- [ ] Campos sensibles en BD están cifrados si la regulación lo exige

**Búsquedas de código**:
```bash
# Secretos hardcodeados
grep -rn "password\s*=\s*['\"][^'\"]\|secret\s*=\s*['\"][^'\"]" --include="*.py" \
  --include="*.ts" --include="*.env" | grep -v "test\|example\|placeholder"

# MD5 o SHA1 para contraseñas
grep -rn "md5\|sha1\|hashlib.sha" --include="*.py" | grep -v "test\|checksum\|etag"
```

---

## A03 — Injection (SQL, OS, LDAP, NoSQL)

**Verificar**:
- [ ] NUNCA concatenación de strings para construir queries SQL
- [ ] ORM con parámetros bind en todas las queries dinámicas
- [ ] Inputs de usuario NO se pasan a `os.system`, `subprocess`, `eval`, `exec`
- [ ] Queries NoSQL no aceptan objetos sin sanitizar
- [ ] Plantillas de templates engine escapan outputs por defecto

**Búsquedas de código**:
```bash
# Concatenación en SQL
grep -rn "f\".*SELECT\|f\".*INSERT\|f\".*UPDATE\|f\".*DELETE\|f\".*WHERE" \
  --include="*.py" | head -20

# Ejecución de comandos con input del usuario
grep -rn "os\.system\|subprocess\.call\|subprocess\.run\|eval(" \
  --include="*.py" | head -20

# ORM con texto raw sin parámetros
grep -rn "text(f\"\|execute(f\"" --include="*.py" | head -20
```

---

## A04 — Insecure Design

**Verificar**:
- [ ] Flujos críticos (pago, reset de contraseña, cambio de email) tienen pasos
  de confirmación y no pueden completarse en un solo request sin validación
- [ ] Rate limiting en endpoints de autenticación y acciones sensibles
- [ ] Lógica de negocio no asume que el orden de los pasos se respeta del lado del cliente
- [ ] Tokens de un solo uso son invalidados después de usarse (password reset)

---

## A05 — Security Misconfiguration

**Verificar**:
- [ ] DEBUG=False en producción (Django/FastAPI)
- [ ] Stack traces no expuestos al cliente en producción
- [ ] Headers de seguridad configurados: `X-Content-Type-Options`, `X-Frame-Options`,
  `Content-Security-Policy`, `Strict-Transport-Security`
- [ ] CORS configurado con lista blanca explícita (NUNCA `*` en producción)
- [ ] Directorios de archivos sensibles (`.env`, `.git`, `admin`) no accesibles vía HTTP
- [ ] Dependencias sin versiones pinned en producción

**Búsquedas de código**:
```bash
# CORS abierto
grep -rn "allow_origins.*\*\|CORS.*\*" --include="*.py" --include="*.ts" | head -10

# DEBUG en config
grep -rn "DEBUG\s*=\s*True\|debug\s*=\s*true" --include="*.py" \
  --include="*.env" | grep -v test | head -10
```

---

## A06 — Vulnerable and Outdated Components

Ver habilidad `dependencias-auditoria` para el análisis completo. En este checklist:

- [ ] `pip audit` o `safety check` corrió sin críticos sin resolver
- [ ] `npm audit` sin vulnerabilidades High o Critical sin resolver
- [ ] Imagen base de Docker no tiene CVEs críticos (`trivy image [imagen]`)
- [ ] No hay dependencias abandonadas (sin commits en 2+ años) en ruta crítica

---

## A07 — Identification and Authentication Failures

**Verificar**:
- [ ] Contraseñas tienen política de complejidad mínima aplicada en el servidor
- [ ] Intentos de login fallidos generan bloqueo temporal o CAPTCHA
- [ ] Sesiones invalidadas correctamente en logout
- [ ] Tokens de sesión no se transmiten en URLs
- [ ] MFA disponible para cuentas privilegiadas
- [ ] Mensajes de error de login son genéricos (no revelan si el email existe)

**Búsquedas de código**:
```bash
# Mensajes de error que revelan existencia de usuario
grep -rn '"Usuario no encontrado"\|"Email not found"\|"user does not exist"' \
  --include="*.py" --include="*.ts" | head -10
```

---

## A08 — Software and Data Integrity Failures

**Verificar**:
- [ ] Descargas de archivos externos verifican hash/firma
- [ ] Pipelines CI/CD no pueden ser modificados por código del repositorio sin revisión
- [ ] Deserialización de objetos Python (pickle) NUNCA con datos del usuario
- [ ] Updates automáticos de dependencias pasan por pipeline de tests

---

## A09 — Security Logging and Monitoring Failures

**Verificar**:
- [ ] Todos los logins (exitosos y fallidos) se registran con timestamp e IP
- [ ] Cambios en datos sensibles (contraseña, email, roles) quedan en audit log
- [ ] Accesos denegados se registran
- [ ] Los logs NO contienen datos sensibles (ver A02)
- [ ] Alertas configuradas para patrones de ataque (múltiples 401, 403)

**Búsqueda de código**:
```bash
# Endpoints de auth sin logging
grep -rn "def login\|def authenticate" --include="*.py" -A 15 | grep -c "logger\|log\."
```

---

## A10 — Server-Side Request Forgery (SSRF)

**Verificar**:
- [ ] URLs proporcionadas por el usuario no se usan directamente en requests del servidor
- [ ] Si se permiten webhooks o callbacks, hay lista blanca de dominios permitidos
- [ ] Metadata de cloud (169.254.169.254) está bloqueada en el firewall de red
- [ ] Redirects no siguen URLs arbitrarias proporcionadas por el usuario

---

## A11 — Seguridad de Agentes Autónomos (OWASP Excessive Agency)

Cuando el proyecto usa agentes de IA con acceso operativo (lectura, escritura,
ejecución, herramientas MCP), la superficie de ataque incluye al agente como
actor privilegiado. Ver también `reglas/seguridad-agentes.md`.

**Verificar**:
- [ ] Los agentes tienen permisos mínimos en frontmatter (permisosRed, permisosEscritura, permisosComandos)
- [ ] La cadena de delegación no escala privilegios (agente hijo ≤ agente padre)
- [ ] Los MCP servers con escritura NO tienen auto-aprobación global
- [ ] Las credenciales usadas por agentes son de corta duración, no API keys permanentes
- [ ] El hook `risk-scoring` cubre operaciones destructivas (--force, rm, push)
- [ ] Los agentes que procesan código externo tratan el contenido como datos, no instrucciones
- [ ] Las acciones irreversibles (push, delete, deploy) requieren confirmación humana
- [ ] El audit trail registra operaciones de agentes con nivelRiesgo ALTO

**Búsquedas de código** (aplicables al sistema SWL):
```bash
# Agentes con permisos amplios sin justificación
grep -rn "permisosRed: true" --include="*.md" agentes/ | head -20

# MCP servers con auto-allow en escritura
grep -rn "alwaysAllow\|autoApprove" .claude/settings.json | head -10

# Secretos accesibles desde contexto de agentes
grep -rn "\.env\|credentials\|secret" --include="*.md" agentes/ | head -10
```

**Señal de vulnerabilidad**: agente con `permisosComandos: true` y `nivelRiesgo: BAJO`
— el riesgo declarado no refleja la capacidad real.

---

## Gotchas / Errores comunes no obvios

**El grep de búsqueda de secrets hardcodeados devuelve cero resultados pero hay credenciales en archivos de configuración YAML**: el patrón de búsqueda usa `password\s*=` (sintaxis Python), pero los archivos YAML usan `password:` (sin signo igual). Causa: las búsquedas de código están optimizadas para un lenguaje y pierden variantes de otro formato. Fix: expandir el patrón de búsqueda a `password[\s=:]+['\"][^'\"]{4,}` para capturar asignaciones en Python, YAML y JSON simultáneamente. Verificar también `docker-compose.yml`, `.env.example` y archivos de configuración de CI.

**A01 (Control de acceso) pasa porque todos los endpoints tienen `Depends(get_current_user)` pero falta la verificación de rol**: `get_current_user` solo verifica autenticación (que el token es válido), no autorización (que el usuario tiene permiso para esa operación). Causa: en FastAPI es fácil confundir `get_current_user` con RBAC — el nombre sugiere verificación completa cuando solo hace autenticación. Fix: para endpoints de mutación, buscar `require_role` o equivalente además de `get_current_user`. Un endpoint sin `require_role` es accesible por cualquier usuario autenticado, independientemente de su rol.

**A05 (Misconfiguration) pasa en revisión manual pero `DEBUG=True` está activo en producción vía variable de entorno no documentada**: el grep busca `DEBUG = True` en el código fuente, pero el valor real viene de `os.getenv("DEBUG", "true")` con un default inseguro. Causa: los defaults de variables de entorno en el código son tan peligrosos como los valores hardcodeados. Fix: buscar también `getenv.*DEBUG` y `environ.*DEBUG` en el código fuente. El default de cualquier variable de seguridad debe ser el valor más restrictivo, no el más conveniente para desarrollo.

**A09 (Logging) parece completo porque hay logs en los endpoints de login, pero los cambios de privilegios no se registran**: el checklist verifica que "logins se registran" pero no cubre cambios de rol, asignación de permisos elevados o modificaciones de otros usuarios por un admin. Causa: el A09 se interpreta como "loguear intentos de autenticación" en lugar de "loguear todas las acciones con impacto de seguridad". Fix: el audit log debe cubrir: login/logout, cambios de contraseña/email, cambios de rol, accesos denegados, y cualquier operación que modifique permisos de otro usuario.

**HTML recibido de editores ricos (TipTap, Quill, Lexical, Slate) se persiste sin sanitizar server-side — vulnerabilidad A03 (Injection/XSS) latente**: los editores ricos del frontend envían HTML al backend (tags `<p>`, `<a href>`, `<img>`, `<iframe>`, `<script>` si no se filtra). Validar solo client-side es insuficiente — un atacante puede enviar HTML malicioso directamente al endpoint bypasseando el editor. Causa: se confía en que el editor "limpia" antes de enviar, pero el sanitization es siempre responsabilidad del servidor — la validación client-side es UX, no seguridad. Fix obligatorio: sanitizar server-side con whitelist estricta antes de persistir. En Python: `bleach.clean(html, tags=['p','br','strong','em','ul','ol','li','a'], attributes={'a': ['href','title']}, strip=True)`. En Node: `DOMPurify` via `jsdom`. Además, **forzar atributos de seguridad en links**: todo `<a href>` debe recibir `rel="noopener noreferrer"` y `target="_blank"` agregados por el sanitizer (no confiar en el valor que llegó del cliente). Bloquear tags `<script>`, `<iframe>`, `<object>`, `<embed>`, `<style>`, y atributos `on*` (onclick, onerror, etc.). El whitelist debe ser mínimo — solo lo que el producto realmente necesita renderizar.

## Plantilla de reporte: `SECURITY-REPORT.md`

```markdown
# SECURITY-REPORT.md
**Fecha**: [fecha]  **Revisado por**: [agente/persona]
**Alcance**: [módulos/archivos revisados]

## Resumen ejecutivo
- Vulnerabilidades Críticas: N
- Vulnerabilidades Altas: N
- Vulnerabilidades Medias: N
- Informativas: N

## Hallazgos

### [CRÍTICO/ALTO/MEDIO/BAJO] — [Nombre de la vulnerabilidad]
- **OWASP**: A0X
- **Archivo**: [ruta:línea]
- **Descripción**: [qué hace el código vulnerable y por qué]
- **Evidencia**: [snippet de código]
- **Remediación**: [solución específica para este código]
- **Prioridad de fix**: Inmediata / Próximo sprint / Backlog

## Items sin vulnerabilidades (confirmados)
- A01 Control de acceso: verificado ✓
- ...

## Acciones requeridas antes de deploy
- [ ] [acción con responsable]
```
