Skip to content

Checklist antes de entregar ​

Llegó el momento de entregar el proyecto al cliente. Antes de pasar a producción, un profesional hace una revisión de seguridad sistemática. Esta lección reúne todo el curso en una checklist exhaustiva que puedes copiar a cada proyecto, más scripts y herramientas para comprobar los puntos más importantes en minutos.

Concepto ​

Los pilotos de avión, con miles de horas de vuelo, siguen usando una checklist antes de despegar. No porque no sepan volar, sino porque la memoria falla y un solo olvido puede ser catastrófico. En seguridad web pasa lo mismo: casi todas las filtraciones vienen de un descuido simple (un display_errors encendido, una carpeta de respaldo pública, una contraseña por defecto), no de un ataque sofisticado.

Úsala así:

  1. Copia la lista a un archivo SEGURIDAD.md del proyecto o a tu gestor de tareas.
  2. Revísala en el entorno de producción (o uno idéntico), no solo en local.
  3. Lo que no aplique, márcalo como "N/A" con una razón. Lo que no se pueda cumplir, documéntalo y explícaselo al cliente.

1. Configuración del servidor y entorno ​

  • [ ] HTTPS activo con certificado válido y renovación automática.
  • [ ] Todo HTTP redirige a HTTPS con 301.
  • [ ] Modo debug apagado: display_errors = Off, APP_DEBUG=false, NODE_ENV=production.
  • [ ] Los errores se registran en logs, no se muestran al usuario.
  • [ ] Páginas de error personalizadas (404, 500) sin trazas ni rutas internas.
  • [ ] expose_php = Off, server_tokens off, sin cabecera X-Powered-By.
  • [ ] Listado de directorios desactivado (Options -Indexes).
  • [ ] El document root apunta a public/; el código, vendor/ y .env están fuera.
  • [ ] Versiones soportadas de PHP (8.2+) / Node (LTS) y del sistema operativo.
  • [ ] Permisos de archivos correctos: carpetas 755, archivos 644, .env 600 o 640; nada en 777.
  • [ ] Solo los puertos necesarios abiertos (80, 443 y SSH); MySQL no expuesto a internet.
  • [ ] SSH con llaves, sin login de root por contraseña (en VPS).

2. Archivos que no deben ser públicos ​

Comprueba que estas URLs devuelven 403 o 404:

  • [ ] /.env
  • [ ] /.git/config
  • [ ] /composer.json, /composer.lock, /package.json
  • [ ] /vendor/ y /node_modules/
  • [ ] /phpinfo.php, /info.php, /test.php (bórralos)
  • [ ] Respaldos: /backup.sql, /backup.zip, /sitio.tar.gz, /config.php.bak, /config.php~
  • [ ] /storage/logs/, /logs/, /error_log
  • [ ] Herramientas de administración olvidadas: /phpmyadmin, /adminer.php

3. Autenticación ​

  • [ ] Contraseñas con password_hash / bcrypt / Argon2id. Ninguna en texto plano, MD5 o SHA.
  • [ ] Longitud mínima de 10-12 caracteres.
  • [ ] Límite de intentos de login (rate limiting) y bloqueo temporal.
  • [ ] Mensajes genéricos en login, registro y recuperación ("correo o contraseña incorrectos").
  • [ ] Recuperación de contraseña con token aleatorio, de un solo uso, que expira y se guarda como hash.
  • [ ] Se eliminaron usuarios de prueba y cuentas admin/admin.
  • [ ] La cuenta del cliente usa una contraseña generada y entregada por canal seguro.
  • [ ] 2FA disponible al menos para administradores (si el proyecto lo justifica).
  • [ ] Reautenticación antes de acciones críticas (cambiar correo, contraseña, borrar cuenta).

4. Sesiones y cookies ​

  • [ ] Cookies de sesión con HttpOnly, Secure y SameSite=Lax (o Strict).
  • [ ] session_regenerate_id(true) / req.session.regenerate() al iniciar sesión.
  • [ ] Expiración por inactividad configurada.
  • [ ] El logout destruye la sesión en el servidor y borra la cookie.
  • [ ] En Express, almacén de sesiones persistente (Redis/BD), no MemoryStore.
  • [ ] Tokens JWT (si se usan) fuera de localStorage, con expiración corta.

5. Control de acceso ​

  • [ ] Todas las rutas privadas verifican la sesión en el servidor.
  • [ ] Cada consulta de datos privados filtra por el dueño (WHERE usuario_id = ?).
  • [ ] Roles y permisos verificados en cada acción, no solo ocultando botones.
  • [ ] Probado: cambiar IDs en URLs y formularios no da acceso a datos ajenos (IDOR).
  • [ ] Las rutas de administración no son accesibles para usuarios normales.
  • [ ] El panel de administración tiene una URL no obvia o restricción por IP (opcional, como capa extra).

6. Entrada de datos e inyecciones ​

  • [ ] Todas las consultas SQL usan consultas preparadas; cero concatenación.
  • [ ] ORDER BY y nombres de columnas dinámicos usan listas blancas.
  • [ ] Toda salida de datos del usuario se escapa (htmlspecialchars, plantillas con autoescape, textContent).
  • [ ] Sin innerHTML, v-html o dangerouslySetInnerHTML con datos del usuario.
  • [ ] HTML enriquecido sanitizado con HTML Purifier / sanitize-html.
  • [ ] Validación en el servidor de todos los campos (tipo, longitud, formato, listas blancas).
  • [ ] Protección contra mass assignment (campos permitidos explícitos / $fillable).
  • [ ] Sin eval, exec/system con datos del usuario; si es imprescindible, escapeshellarg/execFile.
  • [ ] Rutas de archivos protegidas contra ../ (basename, realpath).
  • [ ] Peticiones a URLs externas indicadas por el usuario validadas contra SSRF.

7. CSRF ​

  • [ ] Todos los formularios que modifican datos llevan token CSRF.
  • [ ] Peticiones AJAX envían el token en una cabecera.
  • [ ] Ninguna acción que modifica datos se ejecuta con GET (borrar, cerrar sesión, cambiar estado).

8. Subida de archivos ​

  • [ ] Lista blanca de tipos verificando el contenido real (finfo, file-type).
  • [ ] Archivos renombrados con nombres aleatorios.
  • [ ] Guardados fuera de la carpeta pública o en carpeta sin ejecución de scripts.
  • [ ] Límite de tamaño en el servidor (PHP/Nginx/multer).
  • [ ] SVG y HTML de usuarios bloqueados o servidos como descarga.
  • [ ] Probado: subir un .php renombrado a .jpg es rechazado.

9. Cabeceras HTTP ​

  • [ ] Strict-Transport-Security
  • [ ] Content-Security-Policy (al menos frame-ancestors, object-src 'none', base-uri 'self')
  • [ ] X-Content-Type-Options: nosniff
  • [ ] X-Frame-Options: DENY o SAMEORIGIN
  • [ ] Referrer-Policy: strict-origin-when-cross-origin
  • [ ] Permissions-Policy
  • [ ] Calificación A o superior en securityheaders.com.

10. APIs y CORS ​

  • [ ] CORS con lista blanca exacta de orígenes; sin reflejar Origin con credenciales.
  • [ ] Autenticación en todas las rutas privadas de la API.
  • [ ] Rate limiting en la API, especialmente en login, registro, recuperación y formularios de contacto.
  • [ ] Respuestas de error sin detalles internos.
  • [ ] La API no devuelve campos sensibles (password_hash, tokens, datos de otros usuarios).
  • [ ] Límite de tamaño del cuerpo (express.json({ limit }), post_max_size).

11. Secretos ​

  • [ ] Ningún secreto en el código ni en el frontend.
  • [ ] .env en .gitignore y no accesible por web.
  • [ ] El historial de Git no contiene secretos (gitleaks detect).
  • [ ] Claves de producción distintas a las de desarrollo; claves de prueba de Stripe/PayPal sustituidas por las reales.
  • [ ] Usuario de base de datos propio de la app con permisos mínimos (no root).
  • [ ] Contraseña de BD larga y aleatoria.
  • [ ] Credenciales entregadas al cliente por un gestor de contraseñas o enlace de un solo uso.

12. Dependencias ​

  • [ ] composer audit sin vulnerabilidades altas/críticas.
  • [ ] npm audit --omit=dev sin vulnerabilidades altas/críticas.
  • [ ] CMS, temas y plugins (WordPress, etc.) actualizados; eliminados los que no se usan.
  • [ ] Archivos de bloqueo (composer.lock, package-lock.json) incluidos en el repositorio.
  • [ ] Scripts de CDN externos con versión fija (y integrity cuando sea posible).

13. Respaldos, logs y monitoreo ​

  • [ ] Respaldos automáticos de base de datos y archivos subidos, fuera del servidor.
  • [ ] Se probó restaurar un respaldo (un respaldo no probado no es un respaldo).
  • [ ] Logs de errores y de accesos activos y rotados.
  • [ ] Registro de eventos de seguridad: logins fallidos, cambios de contraseña, acciones de admin.
  • [ ] Monitoreo de disponibilidad (UptimeRobot o similar) con aviso por correo.
  • [ ] Alertas de vencimiento de dominio y certificado.

14. Privacidad y aspectos legales ​

  • [ ] Aviso de privacidad publicado (en México, conforme a la ley de protección de datos personales; en la UE, RGPD).
  • [ ] Solo se recolectan los datos personales necesarios.
  • [ ] Banner de cookies si se usan cookies de analítica o publicidad (según el país).
  • [ ] Nunca se guardan datos completos de tarjetas: el pago lo procesa la pasarela (Stripe, Mercado Pago, PayPal).

Scripts de verificación rápida ​

bash
#!/usr/bin/env bash
# Uso: ./revisar-sitio.sh https://tusitio.com
SITIO="$1"

echo "== Cabeceras de seguridad =="
curl -sI "$SITIO" | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|x-powered-by|^server:'

echo
echo "== Redirección HTTP → HTTPS =="
curl -sI "${SITIO/https:/http:}" | grep -iE '^HTTP|^location'

echo
echo "== Archivos que deberían dar 403/404 =="
for ruta in .env .git/config composer.json composer.lock package.json phpinfo.php info.php \
            backup.sql backup.zip error_log vendor/ storage/logs/ adminer.php phpmyadmin/; do
  codigo=$(curl -s -o /dev/null -w "%{http_code}" "$SITIO/$ruta")
  if [[ "$codigo" == "200" ]]; then
    echo "❌ $codigo  /$ruta  ← ¡expuesto!"
  else
    echo "✅ $codigo  /$ruta"
  fi
done
bash
# PHP
composer audit
composer outdated --direct

# Node
npm audit --omit=dev
npm outdated

# Secretos en el repositorio
gitleaks detect --source . -v
bash
# Consultas con variables concatenadas (PHP)
grep -rnE "(query|exec)\(.*\\\$" --include=*.php . | grep -v vendor/

# Salida sin escapar en vistas PHP
grep -rn "<?= \$" --include=*.php . | grep -v "e(" | grep -v vendor/

# innerHTML y eval en JavaScript
grep -rnE "innerHTML|eval\(|document\.write" --include=*.js src/

# Restos de depuración
grep -rnE "var_dump|print_r|console\.log|dd\(" --include=*.{php,js} . | grep -vE "vendor/|node_modules/"

Herramientas gratuitas útiles

  • securityheaders.com y SSL Labs: cabeceras y TLS.
  • OWASP ZAP: escáner automático de vulnerabilidades (úsalo solo en tus sitios).
  • Mozilla Observatory: revisión general de configuración.
  • gitleaks: secretos en el repositorio.
  • WPScan: si el proyecto es WordPress.

Documento de entrega al cliente ​

Además del código, entrega un documento breve con:

txt
ENTREGA DE PROYECTO — SEGURIDAD
================================
Proyecto: Tienda Ejemplo           Fecha: 18/10/2026

1. Accesos entregados (vía gestor de contraseñas):
   - Panel de administración: https://tienda.com/admin
   - Hosting / cPanel, base de datos, correo SMTP, pasarela de pago

2. Medidas implementadas:
   - HTTPS con renovación automática y HSTS
   - Contraseñas con bcrypt, límite de intentos de login
   - Protección CSRF, XSS e inyección SQL
   - Cabeceras de seguridad (calificación A en securityheaders.com)
   - Respaldos diarios automáticos (retención 30 días) en almacenamiento externo

3. Responsabilidades del cliente:
   - No compartir contraseñas; activar 2FA en el panel
   - Contratar mantenimiento para actualizaciones mensuales
   - Avisar de inmediato ante actividad sospechosa

4. Pendientes / riesgos aceptados:
   - (ej.) El hosting no permite cambiar el document root: se protegió con .htaccess

Error común

Entregar y desaparecer. Las vulnerabilidades se descubren todo el tiempo en librerías que hoy son seguras. Ofrece un plan de mantenimiento (actualizaciones, respaldos, monitoreo): protege al cliente y te da un ingreso recurrente.

Ejercicios ​

  1. Ejecuta el script revisar-sitio.sh contra un proyecto tuyo publicado y corrige al menos tres hallazgos.
  2. Recorre las secciones 3 a 7 de la checklist sobre un proyecto PHP que hayas hecho en el curso de PHP y anota qué falta.
  3. Redacta el apartado "Responsabilidades del cliente" para una tienda en línea pequeña.
Ver solución
  1. Respuesta libre. Los hallazgos más comunes y su solución:
apache
# .htaccess: soluciona varios de una vez
Options -Indexes
<FilesMatch "^\.">
    Require all denied
</FilesMatch>
RedirectMatch 404 ^/(vendor|storage|\.git)(/|$)
<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header unset X-Powered-By
</IfModule>
  1. Respuesta libre. En proyectos de práctica suele faltar: límite de intentos de login, session_regenerate_id tras el login, configuración Secure/SameSite de la cookie, tokens CSRF en formularios de borrado y verificación de dueño en las consultas de edición.

  2. Ejemplo:

txt
- Usar contraseñas únicas y activar la verificación en dos pasos en el panel y en el correo.
- No crear cuentas de administrador para personas que no las necesitan; dar de baja a empleados que se van.
- No instalar plugins ni modificar el código sin consultar al desarrollador.
- Mantener vigente el plan de hosting, el dominio y el contrato de mantenimiento.
- Revisar periódicamente los pedidos y avisar de cualquier actividad extraña.

Resumen ​

  • Una checklist evita los descuidos simples que causan la mayoría de las filtraciones.
  • Revisa en producción: configuración, archivos expuestos, autenticación, sesiones, control de acceso, inyecciones, CSRF, archivos, cabeceras, CORS, secretos, dependencias y respaldos.
  • Automatiza lo que puedas con scripts, composer audit, npm audit y gitleaks.
  • Entrega un documento de seguridad y credenciales por canal seguro.
  • Ofrece mantenimiento: la seguridad es un proceso continuo, no un evento.

Academia Web — aprende, copia y construye.