# Regla: Seguridad

Esta regla es OBLIGATORIA. Aplica a todo código de producción sin excepción.
Ninguna feature se considera completa si viola cualquiera de los puntos aquí listados.

---

## OWASP Top 10 — Mitigaciones obligatorias

Antes de cerrar cualquier ticket, verificar que el código no introduce ninguna
vulnerabilidad del OWASP Top 10. Las más frecuentes en este stack:

- **Inyección (A03)**: Toda interacción con BD usa parameterized queries o el ORM.
  NUNCA concatenar strings para construir SQL. NUNCA usar `format()` o f-strings
  en queries de BD.
- **Autenticación rota (A07)**: Tokens con expiración corta. Refresh tokens en
  httpOnly cookies. Invalidar sesiones al cambiar contraseña.
- **Exposición de datos sensibles (A02)**: Cifrar PII en reposo. TLS obligatorio
  en tránsito. Logs sin datos personales ni tokens.
- **Control de acceso roto (A01)**: Verificar permisos server-side en CADA endpoint.
  No confiar en validaciones de frontend para autorización.
- **Componentes vulnerables (A06)**: Actualizar dependencias con CVEs conocidos
  dentro de 72 horas de notificación.

---

## Gestión de secretos

- NUNCA hardcodear credenciales, API keys, tokens, passwords o connection strings
  en el código fuente.
- NUNCA hardcodear secretos en archivos de configuración que se versionen en git
  (.env, config.yaml, settings.py, etc.).
- Usar variables de entorno inyectadas por el sistema de deployment (Vault, AWS
  Secrets Manager, Azure Key Vault, Kubernetes Secrets, etc.).
- Archivo `.env` solo para desarrollo local — SIEMPRE en `.gitignore`.
- Si un secreto se filtra accidentalmente en git: rotarlo inmediatamente y
  limpiar el historial. No es suficiente con un commit que lo borre.
- Auditar `.env.example` periódicamente — solo debe contener nombres de variables,
  NUNCA valores reales.

---

## Validación y sanitización de inputs

- Validar TODA entrada del usuario en el servidor, sin excepción.
  La validación en frontend es UX, no seguridad.
- Usar esquemas Pydantic (Python) o Zod/class-validator (TS) para inputs de API.
- Definir tipos estrictos: longitud máxima, formato, dominio de valores permitidos.
- Rechazar inputs malformados con 400 Bad Request, con mensaje de error genérico
  (no exponer detalles internos de validación al cliente externo).
- Sanitizar HTML antes de renderizar en el frontend si el contenido viene de usuarios.
  Usar DOMPurify o equivalente — NUNCA confiar en strings crudos.
- Normalizar paths antes de operar sobre el filesystem (ver path traversal).

---

## Parameterized queries

- ORM o prepared statements SIEMPRE para consultas a la BD.
- Si se requiere SQL crudo por performance, usar `text()` de SQLAlchemy con
  parámetros nombrados — NUNCA interpolación de strings.
- Revisión obligatoria en code review: buscar `f"SELECT` o `"SELECT" +` como
  señales de alerta inmediata.

---

## HTTPS y seguridad de transporte

- HTTPS obligatorio en todos los ambientes excepto localhost.
- HSTS habilitado con `max-age` mínimo de 1 año.
- Certificados renovados automáticamente (Let's Encrypt / cert-manager).
- Redirigir HTTP → HTTPS en el nivel del balanceador/proxy, no en la app.
- No usar TLS < 1.2. Preferir TLS 1.3.

---

## CORS

- Whitelist explícita de orígenes permitidos — NUNCA `*` en producción.
- Orígenes definidos como variables de entorno, no hardcodeados en el código.
- `credentials: true` solo cuando sea estrictamente necesario.
- Revisar que los métodos y headers permitidos son los mínimos necesarios.

---

## CSRF

- CSRF tokens obligatorios en toda operación de mutación (POST, PUT, PATCH, DELETE)
  para sesiones basadas en cookies.
- APIs stateless con JWT en Authorization header son inmunes a CSRF por diseño,
  pero documentar explícitamente por qué no se necesita el token.
- Verificar el header `Origin` o `Referer` como defensa adicional.
- SameSite=Strict o SameSite=Lax en cookies de sesión.

---

## Sanitización de output

- Escapar HTML en templates server-side (Jinja2 lo hace por defecto — no desactivar).
- En Angular, usar interpolación `{{ }}` en lugar de `[innerHTML]`.
  Si `[innerHTML]` es necesario, sanitizar con `DomSanitizer.sanitize()`.
- No confiar en que el frontend sanitiza — validar también server-side.
- Cabeceras de seguridad obligatorias en respuestas HTTP:
  `Content-Security-Policy`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`.

---

## Funciones peligrosas prohibidas

- `eval()` — prohibido en JavaScript y Python sin excepción.
- `exec()` — prohibido en Python sin revisión de seguridad explícita documentada.
- `subprocess` con `shell=True` — prohibido. Usar lista de argumentos.
- `pickle.loads()` con datos externos — prohibido (deserialización insegura).
- `yaml.load()` sin `Loader=yaml.SafeLoader` — prohibido.
- `__import__()` con strings dinámicos — prohibido.

---

## Path traversal prevention

- Normalizar y validar paths antes de operaciones de filesystem:
  ```python
  from pathlib import Path
  base = Path("/uploads").resolve()
  target = (base / user_input).resolve()
  assert str(target).startswith(str(base)), "Path traversal detectado"
  ```
- NUNCA usar input del usuario directamente en `open()`, `os.path.join()` o
  equivalentes sin normalización previa.
- Directorios de uploads aislados del código fuente de la aplicación.

---

## Actualización de dependencias

- Ejecutar `pip-audit` / `npm audit` en CI. Fallo en vulnerabilidades HIGH o CRITICAL.
- Dependabot o Renovate configurado en el repositorio.
- Dependencias con CVE crítico: actualizar en máximo 72 horas.
- Dependencias con CVE medio: actualizar en máximo 2 semanas.
- Pin versions en producción — no usar rangos abiertos (`>=`, `*`, `^` en prod).

---

## Verificación obligatoria de flags de configuración antes de asumir servicios externos

Toda rama de código que asuma un servicio externo opcional (LDAP, Redis, S3, SMTP,
OIDC, Auth0, etc.) DEBE verificar el flag de configuración del proyecto ANTES de
tomar la decisión. Sin esta verificación, una configuración no esperada deja a los
usuarios atrapados en flujos rotos sin error visible.

**Anti-patrón** (caso real OIC 2026-06-04, dos bugs históricos en el mismo flujo):

```python
# MAL — asume LDAP siempre habilitado
if rol == "ADMIN":
    hashed = hash_password(body.password)
else:
    # "Para usuarios LDAP, no se almacena contraseña funcional local."
    hashed = hash_password(uuid.uuid4().hex)
    # ↑ Si AUTH_LDAP_ENABLED=false (caso real), el usuario queda con un hash
    # UUID dummy que ninguna password real puede igualar → 401 perpetuo.
```

```python
# MAL — rechaza no-ADMIN sin verificar si LDAP existe siquiera
if rol != "ADMIN":
    raise HTTPException(400, "Los usuarios no ADMIN gestionan su contraseña en LDAP")
    # ↑ Sin LDAP habilitado los usuarios no tienen forma de cambiar password.
```

**Patrón correcto**:

```python
# BIEN — siempre verifica el flag antes de asumir el servicio
if rol == "ADMIN":
    hashed = hash_password(body.password)
else:
    if body.password:
        hashed = hash_password(body.password)
    elif not settings.AUTH_LDAP_ENABLED:
        # Sin LDAP: generar password funcional para que el usuario pueda entrar
        temp = _generar_temp_password()
        hashed = hash_password(temp)
        # devolver `temp` en la respuesta del POST UNA SOLA VEZ
    else:
        # LDAP habilitado: dummy OK porque el usuario se autentica fuera
        hashed = hash_password(uuid.uuid4().hex)
```

**Detección temprana en code review**: cualquier comentario `"para LDAP"`,
`"para Redis"`, `"para S3"` debe acompañarse del check del flag correspondiente.
Si el comentario está solo, es bug latente — el código asume un mundo que el
deployment puede no tener.

---

## Sanitización PII centralizada en handlers vs refactorear N sitios

Cuando un sistema persiste logs estructurados (audit trails, error logs,
telemetría) y existen N sitios donde el código emite `logger.error(..., extra={...})`
con potencial PII en los valores, **siempre preferir sanitización centralizada en
el handler** sobre refactorear cada sitio individualmente.

**Decisión arquitectónica** (caso OIC v1.5 Slice 2, 2026-06-04):

| Opción | Esfuerzo | Cobertura futura | Mantenimiento |
|---|---|---|---|
| Refactorear N sitios uno por uno | O(N) turnos | Solo lo refactorizado | Cada `logger.error` nuevo requiere auditoría |
| Sanitización centralizada en handler | O(1) turno | Todos los sitios + futuros automáticamente | Cero auditoría manual al agregar nuevos `logger.error` |

**Patrón portable**:

```python
import re

_PATRON_SENSIBLE = re.compile(
    r'(password|token|secret|jwt|authorization|cookie|api[_-]?key)([=:])([^&\s"\']+)',
    re.IGNORECASE,
)

def sanitizar_url(url: str | None) -> str | None:
    """Redacta query-params sensibles preservando key y separador."""
    if not url:
        return url
    return _PATRON_SENSIBLE.sub(r"\1\2<redacted>", url)

def sanitizar_valor(valor: Any) -> Any:
    """Sanitiza recursivamente strings dentro de dict/list/tuple."""
    if isinstance(valor, str):
        return _PATRON_SENSIBLE.sub(r"\1\2<redacted>", valor)
    if isinstance(valor, dict):
        return {k: sanitizar_valor(v) for k, v in valor.items()}
    if isinstance(valor, (list, tuple)):
        return [sanitizar_valor(item) for item in valor]
    return valor
```

**Regla del regex**:
- Grupo 1 (key) y grupo 2 (`=` o `:`) se preservan → mantiene legibilidad
  (`password=<redacted>` en vez de `<redacted>`).
- Grupo 3 (valor) se reemplaza por `<redacted>`.
- Boundary del valor: `&`, whitespace, comilla, paréntesis.
- `api_key` y `api-key` ambos matchean con `api[_-]?key`.

**Aplicabilidad**: handlers de logging custom, middleware HTTP que redactan headers,
sanitización de cookies en logs, redactor de stack traces, error reporters
(Sentry-style). Defensa en profundidad sin tocar el código del caller.

**Limitación documentada**: el regex opera sobre strings; NO sanitiza claves cuyo
nombre no esté en la lista (no detecta `contraseña` en español, `pwd`, `mot_de_passe`).
Mantener el regex extensible vía configuración si el proyecto opera en múltiples idiomas.

---

## Generación de tokens y passwords legibles (sin caracteres ambiguos)

Para passwords temporales (reset por admin, primer login, recuperación) que el
usuario debe transcribir verbalmente o desde papel, usar alfabeto SIN caracteres
ambiguos: `0` (cero) vs `O` (o mayúscula), `1` (uno) vs `l` (L minúscula) vs `I`
(i mayúscula).

```python
import secrets

# 54 chars del alfabeto seguro (sin 0, O, 1, l, I)
_ALFABETO_LEGIBLE = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"

def generar_password_temporal(longitud: int = 12) -> str:
    """12 chars → ~71 bits de entropía. Suficiente para temporal que debe
    cambiarse en primer login. NUNCA persistir en texto plano; el caller
    la hashea con bcrypt y la devuelve UNA SOLA VEZ en la respuesta del POST."""
    return "".join(secrets.choice(_ALFABETO_LEGIBLE) for _ in range(longitud))
```

**NO usar** `secrets.token_urlsafe()` para passwords visibles al humano: incluye
`-` y `_` que pueden confundir al transcribir. Reservar `token_urlsafe()` para
tokens de sesión, CSRF, magic links — strings que no se leen ni transcriben.

**Para tokens de uso programático** (API keys, session IDs, refresh tokens):
`secrets.token_urlsafe(32)` (32 bytes → 256 bits) o `secrets.token_hex(32)`. Sin
restricción de alfabeto porque el usuario nunca los teclea manualmente.

---

## Checklist antes de merge a main

- [ ] No hay secrets hardcodeados (revisar con `git grep -i "password\|secret\|token\|key"`)
- [ ] Inputs validados con esquemas estrictos
- [ ] RBAC verificado en todos los endpoints nuevos
- [ ] Sin uso de `eval`, `exec`, `shell=True`
- [ ] Logs sin datos sensibles
- [ ] Dependencias nuevas auditadas
- [ ] Cualquier rama que asuma servicio externo (LDAP/Redis/S3) verifica su flag de config
- [ ] Sanitización PII centralizada en handlers (no diseminada por N sitios)
- [ ] Passwords temporales generadas con alfabeto sin chars ambiguos
