StackPractices
intermediate Por Mathias Paulenko

Checklist de Revisión de Seguridad para PRs

Checklist para revisiones de seguridad durante pull requests: validación de entrada, autenticación, autorización, secretos, dependencias, inyección, XSS, CSRF, logging y herramientas automatizadas con ejemplos de código.

Temas: security

Resumen

Este checklist te da pautas de revisión de seguridad para pull requests. Cada PR que modifique código de aplicación, configuración o infraestructura debe pasar una revisión de seguridad antes de fusionarse. Este documento cubre validación de entrada, autenticación, autorización, gestión de secretos, cambios en dependencias, prevención de inyección, XSS, CSRF, logging y herramientas automatizadas. Para la referencia autoritativa, consulta la Guía de Revisión de Código de OWASP.

Flujo de Revisión de Seguridad de PR

flowchart diagram: PR Abierto

1. Validación de Entrada

1.1 Checklist

- [ ] Toda entrada de usuario se valida en el servidor
- [ ] La validación usa listas de permitidos (no listas de bloqueados)
- [ ] La longitud de entrada está acotada (máximo aplicado)
- [ ] El tipo de entrada se verifica (string, int, UUID, etc.)
- [ ] El formato de entrada se valida (regex para emails, URLs, fechas)
- [ ] Las subidas de archivos verifican tipo MIME y extensión
- [ ] Las subidas de archivos limitan el tamaño máximo
- [ ] Las consultas SQL usan sentencias parametrizadas (sin concatenación)
- [ ] Las consultas NoSQL previenen inyección de operadores ($where, $expr)
- [ ] La ejecución de comandos usa shell=False (no shell=True)
- [ ] El parser XML desactiva entidades externas (XXE)
- [ ] El parser JSON limita profundidad y número de claves

1.2 Ejemplo de Código: Validación de Entrada

from pydantic import BaseModel, validator, constr, conint
from typing import Optional
import uuid

class CreateUserRequest(BaseModel):
    username: constr(min_length=3, max_length=32, pattern=r'^[a-zA-Z0-9_]+$')
    email: constr(min_length=5, max_length=254, pattern=r'^[^@]+@[^@]+\.[^@]+$')
    age: conint(ge=13, le=120)
    bio: Optional[constr(max_length=500)] = None

    @validator('username')
    def no_sql_keywords(cls, v):
        # Verificación adicional para palabras SQL (defensa en profundidad)
        blocked = {'select', 'insert', 'update', 'delete', 'drop', 'union'}
        if any(kw in v.lower() for kw in blocked):
            raise ValueError('Username contains reserved word')
        return v

# Uso: la entrada inválida lanza ValidationError
try:
    user = CreateUserRequest(**request.json())
except ValidationError as e:
    return jsonify({'error': 'Invalid input', 'details': e.errors()}), 400

2. Autenticación y Autorización

2.1 Checklist

- [ ] Los nuevos endpoints requieren autenticación (sin acceso sin autenticar a menos que sea explícito)
- [ ] Las verificaciones de autorización se aplican en el servidor (no solo en el cliente)
- [ ] La propiedad del recurso se verifica (verificación IDOR)
- [ ] Los permisos usan denegar por defecto
- [ ] Los cambios de rol requieren autorización de admin
- [ ] Los tokens de sesión se rotan tras un cambio de privilegios
- [ ] Los cambios de contraseña requieren verificación de la contraseña actual
- [ ] Los endpoints de API aplican rate limiting
- [ ] Las operaciones sensibles requieren re-verificación MFA
- [ ] Los tokens JWT se validan (firma, expiración, emisor, audiencia)

2.2 Ejemplo de Código: Verificación de Autorización

from functools import wraps

def authorize(action: str, resource_type: str = None):
    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            user = g.current_user
            resource_id = kwargs.get('id')

            # Verificar permiso
            if not user.can(action, resource_type, resource_id):
                log_denial(user, action, resource_type, resource_id)
                abort(403)

            return f(*args, **kwargs)
        return wrapped
    return decorator

@app.route('/api/orders/<uuid:order_id>', methods=['PUT'])
@authenticate
@authorize('update', 'order')
def update_order(order_id):
    order = Order.query.get_or_404(order_id)
    # Verificación IDOR: confirmar propiedad
    if order.user_id != g.current_user.id and not g.current_user.has_role('admin'):
        abort(403)
    order.update(request.json())
    return jsonify(order.to_dict())

3. Gestión de Secretos

3.1 Checklist

Para una guía detallada sobre rotación de secretos, consulta el runbook de rotación de secretos.

- [ ] No hay secretos, API keys ni contraseñas en el código fuente
- [ ] No hay secretos en archivos de configuración commiteados a git
- [ ] Los secretos se cargan desde variables de entorno o un gestor de secretos
- [ ] Los archivos .env están en .gitignore
- [ ] No hay secretos en logs ni mensajes de error
- [ ] No hay secretos en parámetros URL ni query strings
- [ ] El pipeline CI/CD usa variables secretas (no hardcodeadas)
- [ ] Las imágenes Docker no contienen secretos (usa build args o env en runtime)
- [ ] La herramienta de escaneo de secretos (GitLeaks, TruffleHog) está en el hook pre-commit
- [ ] No hay secretos en comentarios ni documentación

3.2 Escaneo de Secretos en Pre-Commit

La documentación de GitLeaks cubre configuración y reglas personalizadas. La guía de OWASP sobre gestión de secretos recomienda hooks pre-commit como primera línea de defensa.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks
        args: ['--config', '.gitleaks.toml']

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

4. Cambios en Dependencias

4.1 Checklist

- [ ] Las nuevas dependencias provienen de fuentes confiables (registros oficiales)
- [ ] Las nuevas dependencias no tienen vulnerabilidades críticas conocidas
- [ ] Las versiones de dependencias están fijadas (sin wildcards ni rangos)
- [ ] La compatibilidad de licencia se verifica (sin GPL en proyectos comerciales)
- [ ] La dependencia se mantiene activamente (último commit < 1 año)
- [ ] No se añaden dependencias innecesarias (justificar cada nueva dependencia)
- [ ] El escaneo de dependencias (Dependabot, Snyk) pasa
- [ ] Las dependencias transitivas se revisan por problemas conocidos
- [ ] El lock file está actualizado (package-lock.json, poetry.lock, etc.)
- [ ] No se mueven dependencias de dev a producción

4.2 Verificación Automatizada de Dependencias en CI

# GitHub Actions — dependency security check
name: Dependency Security
on: [pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: npm audit
        run: npm audit --audit-level=high --omit=dev
        continue-on-error: false

      - name: Snyk scan
        uses: snyk/actions/node@master
        with:
          command: test
          args: --severity-threshold=high --fail-on=issues
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

      - name: License check
        run: npx license-checker --production --failOn 'GPL-3.0;AGPL-3.0'

5. Prevención de Inyección

5.1 Checklist

- [ ] Las consultas SQL usan sentencias parametrizadas u ORM
- [ ] Sin concatenación de strings para consultas SQL
- [ ] Sin uso de eval() o Function() con entrada de usuario
- [ ] Sin uso de innerHTML con entrada de usuario (usar textContent)
- [ ] Sin uso de document.write() con entrada de usuario
- [ ] Los motores de plantillas usan auto-escaping (Jinja2, Twig, etc.)
- [ ] Los comandos shell usan arrays de argumentos (sin concatenación)
- [ ] Las consultas LDAP escapan caracteres especiales
- [ ] Las consultas XPath usan expresiones parametrizadas
- [ ] Las consultas NoSQL sanitizan operadores $

5.2 Code Examples

# BAD: SQL injection
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# GOOD: parameterized query
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))

# BAD: command injection
import subprocess
subprocess.run(f"ls {user_dir}", shell=True)

# GOOD: argument array
subprocess.run(["ls", user_dir], shell=False)

# BAD: XSS en JavaScript
document.getElementById('output').innerHTML = userInput;

# GOOD: textContent
document.getElementById('output').textContent = userInput;

6. Prevención de XSS y CSRF

6.1 Checklist XSS

- [ ] La codificación de salida se aplica a todo el contenido generado por el usuario
- [ ] El header Content-Security-Policy está configurado (sin unsafe-inline)
- [ ] El header X-XSS-Protection está configurado
- [ ] X-Content-Type-Options: nosniff está configurado
- [ ] Las cookies son HttpOnly y Secure
- [ ] El auto-escaping del framework está activado
- [ ] Sin uso de dangerouslySetInnerHTML (React) o [innerHTML] (Angular)
- [ ] La entrada de usuario en URLs se codifica (encodeURIComponent)
- [ ] Las subidas SVG se sanitizan (eliminar script tags)
- [ ] El renderizado Markdown sanitiza HTML (sin HTML crudo)

6.2 Checklist CSRF

- [ ] El token CSRF se incluye en todos los formularios que cambian estado
- [ ] El token CSRF se valida en el servidor
- [ ] Las cookies usan SameSite=Strict o SameSite=Lax
- [ ] Los endpoints de API usan headers personalizados (no solo cookies)
- [ ] El header Origin/Referer se valida en solicitudes que cambian estado
- [ ] La política CORS es restrictiva (sin orígenes wildcard)
- [ ] El endpoint de login está protegido contra CSRF (login CSRF)
- [ ] El endpoint de logout está protegido contra CSRF

6.3 Ejemplo de Código: Protección CSRF

from flask import session
import secrets

def generate_csrf_token():
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)
    return session['csrf_token']

@app.before_request
def validate_csrf():
    if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
        token = request.headers.get('X-CSRF-Token') or request.form.get('csrf_token')
        if not token or token != session.get('csrf_token'):
            abort(403, description='CSRF token validation failed')

7. Logging y Manejo de Errores

7.1 Checklist

- [ ] Sin datos sensibles en logs (contraseñas, tokens, PII)
- [ ] Los mensajes de error no exponen detalles internos a los usuarios
- [ ] Los stack traces se registran en el servidor, no se muestran a los clientes
- [ ] Los eventos de seguridad se registran (login, acceso denegado, validación de entrada)
- [ ] Los logs incluyen user ID, timestamp, IP de origen y acción
- [ ] La inyección de logs se previene (sanitizar entrada de logs)
- [ ] El manejo de errores no silencia excepciones
- [ ] El modo debug está desactivado en producción
- [ ] Las páginas de error personalizadas no filtran información del servidor
- [ ] Pista de auditoría para todas las operaciones que cambian estado

7.2 Ejemplo de Código: Logging Seguro

import logging
import re

logger = logging.getLogger(__name__)

# Sanitizar entrada de logs para prevenir inyección de logs
def sanitize_for_log(value: str) -> str:
    # Eliminar newlines y carriage returns
    return re.sub(r'[\r\n]', '', str(value))[:200]

def log_security_event(user_id: str, action: str, resource: str, ip: str):
    logger.info(
        "Security event: user=%s action=%s resource=%s ip=%s",
        sanitize_for_log(user_id),
        sanitize_for_log(action),
        sanitize_for_log(resource),
        sanitize_for_log(ip),
    )

# MAL: registrar datos sensibles
logger.info(f"User logged in with password: {password}")  # NUNCA HAGAS ESTO

# BIEN: registrar sin datos sensibles
logger.info(f"User {user_id} logged in from {ip}")

8. Herramientas Automatizadas

8.1 Verificaciones de CI Requeridas

Para un pipeline completo de seguridad CI/CD, consulta la plantilla de seguridad para pipelines CI/CD.

Tool              | Purpose                    | When to run        | Block merge on
──────────────────┼────────────────────────────┼────────────────────┼──────────────────
SAST (Semgrep)    | Static security analysis   | Every PR            | High severity
Snyk / Dependabot | Dependency vulnerabilities | Every PR            | High severity
GitLeaks          | Secret scanning            | Every commit        | Any finding
ESLint security   | JS/TS security rules       | Every PR            | Error level
Bandit (Python)   | Python security linter     | Every PR            | High severity
Trivy             | Container image scan       | Image build         | High severity
OWASP ZAP         | DAST (dynamic scan)        | Nightly on staging  | Critical

8.2 Configuración de Semgrep

La documentación de Semgrep cubre escritura de reglas personalizadas y patrones específicos por lenguaje.

# semgrep.yml — custom security rules
rules:
  - id: no-hardcoded-secrets
    pattern: password = "..."
    message: "Hardcoded password detected"
    severity: ERROR
    languages: [python, javascript, java]

  - id: no-sql-injection
    pattern: cursor.execute(f"...")
    message: "Possible SQL injection — use parameterized queries"
    severity: ERROR
    languages: [python]

  - id: no-shell-true
    pattern: subprocess.run(..., shell=True)
    message: "shell=True is dangerous with user input"
    severity: WARNING
    languages: [python]

  - id: no-innerhtml
    pattern: $EL.innerHTML = $INPUT
    message: "innerHTML with user input causes XSS — use textContent"
    severity: ERROR
    languages: [javascript]

See Also

Preguntas frecuentes

¿Cuándo debería solicitar una revisión de seguridad?

Solicita una revisión de seguridad para cualquier PR que: añada lógica de autenticación o autorización, maneje entrada de usuario, modifique configuración de seguridad, añada nuevas dependencias, cambie patrones de acceso a datos, use crypto o manejo de tokens, modifique pipelines CI/CD o toque configuración de infraestructura. Para cambios rutinarios (estilos de UI, documentación, pruebas), una auto-revisión contra este checklist es suficiente. Cuando dudes, solicita una revisión: es más barato que un incidente de seguridad.

¿Cuánto debería tomar una revisión de seguridad?

Una revisión de seguridad para un PR típico (100-500 líneas cambiadas) debería tomar 30-60 minutos. Para PRs más grandes o cambios sensibles a seguridad (auth, crypto, pagos), permite 2-4 horas. El revisor debería ejecutar las herramientas automatizadas primero (SAST, escaneo de dependencias), luego revisar manualmente los items del checklist. Si el PR introduce un riesgo de seguridad significativo, programa una sesión de revisión dedicada con el equipo de seguridad en lugar de hacerlo en línea.

¿Qué debería hacer si encuentro un problema de seguridad durante la revisión?

Si el problema es crítico (ejecución remota de código, bypass de autenticación, exposición de datos), bloquea el PR y notifica al equipo de seguridad inmediatamente. No fusiones hasta que el problema se corrija. Si el problema es de severidad alta (inyección SQL, XSS, control de acceso), solicita cambios y explica la corrección necesaria. Si el problema es medio o bajo, añade un comentario con la recomendación y deja que el autor decida si corregirlo en este PR o en un follow-up. Nunca apruebes un PR con problemas de seguridad críticos o altos conocidos.

¿Cómo balanceo la revisión de seguridad con la velocidad de desarrollo?

Automatiza tanto como sea posible: SAST, escaneo de dependencias y escaneo de secretos deberían ejecutarse automáticamente en CI y bloquear PRs con hallazgos críticos. Esto captura los problemas más comunes sin revisión manual. Para la revisión manual, enfócate en las áreas de alto riesgo: autenticación, autorización, validación de entrada y secretos. Usa este checklist como referencia rápida; no revises cada línea para cada item. Prioriza basándote en el perfil de riesgo del PR. Un PR que añade un nuevo endpoint de API necesita más revisión de seguridad que uno que cambia colores de botones.

¿Qué herramientas deberían estar en mi pipeline de seguridad CI?

Como mínimo: SAST (Semgrep o SonarQube), escaneo de dependencias (Snyk o Dependabot) y escaneo de secretos (GitLeaks). Añade DAST (OWASP ZAP) para entornos de staging. Añade escaneo de contenedores (Trivy) si construyes imágenes Docker. Añade verificación de licencias (license-checker) para compliance. Configura todas las herramientas para fallar el build de CI en hallazgos de severidad alta. Ejecuta SAST y escaneo de secretos en cada PR. Ejecuta escaneo de dependencias en cada PR y nocturnamente. Ejecuta DAST nocturnamente contra staging. Revisa los hallazgos de herramientas semanalmente para ajustar falsos positivos.