Seguridad
Modelo de amenazas, contraseñas, sesiones, secretos y lista de comprobación.
Qué protegemos
Ordenado por daño si se filtra:
- Códigos de acceso a propiedades. Filtrarlos equivale a entregar la llave de la casa de un cliente. Es el activo más sensible del sistema.
- Fotografías de propiedades privadas. Muestran el interior de casas ajenas.
- Credenciales de acceso. Dan control sobre todo lo anterior.
- Datos financieros. Tarifas, salarios y márgenes de un negocio privado.
- Datos de contacto de clientes.
Modelo de amenazas
| Amenaza | Mitigación |
|---|---|
| Un empleado curiosea servicios de otro | Filtro por asignación en el WHERE de SQL; se devuelve 404 |
| Un empleado manipula un id en la URL | Guardas centrales; 404 indistinguible de «no existe» |
| Un empleado intenta ver códigos de una propiedad ajena | Los campos se eliminan en el servidor antes de serializar |
| Un empleado intenta ver su tarifa o el precio | Esos campos no se seleccionan en sus consultas |
| Fuerza bruta contra el login | Rate limiting en base de datos por usuario y por IP |
| Enumeración de usuarios | Mensaje único; verificación contra un hash señuelo |
| Robo de cookie de sesión | httpOnly, secure, sameSite lax; revocación inmediata disponible |
| Un ex-empleado conserva acceso | Desactivar la cuenta revoca todas sus sesiones al instante |
| Volcado de la base de datos | Contraseñas con Argon2id; sesiones guardadas como SHA-256 |
| Acceso directo a la evidencia en R2 | Bucket privado; sólo URLs firmadas de 10 minutos |
| Subida de un archivo malicioso | Tipo y tamaño firmados en la URL; lista blanca estrecha |
| Path traversal en el nombre del archivo | La clave del objeto la genera el servidor, con UUID |
| CSRF | Server Actions con validación de origen; cookies sameSite |
| Un secreto acaba en un log | Saneado automático por nombre de clave en logger y auditoría |
| Un secreto acaba en la imagen de Docker | .dockerignore excluye todo .env |
Autenticación
Contraseñas. Argon2id con los parámetros recomendados por OWASP: 19 MiB de memoria, 2 iteraciones, paralelismo 1. Son suficientemente costosos para frenar el cracking por GPU sin ahogar una VPS modesta. El hash es autodescriptivo, así que subir los parámetros más adelante no invalida los existentes.
Política. Mínimo 8 caracteres y prohibición de las contraseñas triviales más habituales. Se evita exigir símbolos y mayúsculas a propósito: el personal de campo trabaja desde el teléfono y esas reglas empujan a escribir la contraseña en un papel, que es peor.
Enumeración de usuarios. El login siempre responde «Usuario o contraseña incorrectos». Además, cuando el usuario no existe se verifica la contraseña contra un hash señuelo real, calculado con los mismos parámetros, para que el tiempo de respuesta no delate qué cuentas existen.
Rate limiting. 5 intentos fallidos por usuario y 20 por IP en 15 minutos. Se comprueba antes de ejecutar Argon2, para que verificar el hash no se convierta en el propio vector de agotamiento de CPU.
Sesiones
- Token de 32 bytes aleatorios, en una cookie
httpOnly,secureen producción ysameSite=lax. - La base guarda el SHA-256 del token: un volcado de la tabla no permite suplantar a nadie.
- Duración de 30 días con renovación deslizante.
- Un usuario inactivo o archivado se trata como no autenticado aunque su cookie siga siendo válida.
Se revocan todas las sesiones al desactivar una cuenta, al restablecer una contraseña y al cambiarla (excepto la sesión desde la que se actúa).
Autorización
Documentada por completo en AUTHORIZATION.md.
Las dos ideas centrales: la comprobación ocurre siempre en el servidor, y un recurso ajeno devuelve 404, no 403.
Evidencia y almacenamiento
- Bucket privado. No existe ninguna URL pública permanente.
- Ver una foto exige pasar por
/api/evidence/{id}, que comprueba el acceso al servicio y sólo entonces redirige a una URL firmada de 10 minutos. - Las URLs de subida duran 15 minutos y llevan el tipo y el tamaño firmados: el cliente no puede subir algo distinto de lo autorizado.
- La clave del objeto la genera el servidor con un UUID. El nombre del archivo del usuario nunca se usa como ruta.
- Sólo se aceptan
image/webp,image/jpegy tres formatos de vídeo. - Confirmar una subida verifica con
HEADque el objeto existe de verdad.
Detalle en R2_STORAGE.md.
Privacidad del personal
- No se rastrea la ubicación. El único uso de mapas es abrir la dirección de la propiedad en Google Maps.
- La compresión en el navegador descarta los metadatos EXIF, incluidas las coordenadas GPS y el modelo del dispositivo. Esos datos no salen del teléfono.
- La cabecera
Permissions-Policydeshabilita geolocalización y micrófono.
Registro
src/lib/logger.ts emite JSON por línea y sanea automáticamente por nombre
de clave: contraseñas, hashes, tokens, secretos, cookies, credenciales y códigos
de acceso se sustituyen por [redactado].
Las URLs firmadas se recortan antes de la firma: una URL completa en un log sería reutilizable mientras no expire.
La auditoría aplica el mismo saneado. Que un código de puerta cambie sí se audita; su valor, no.
Cabeceras HTTP
Configuradas en next.config.ts:
| Cabecera | Valor |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | geolocation=(), microphone=(), payment=(), camera=(self) |
poweredByHeader está desactivado.
Secretos
- Nunca en el repositorio.
.gitignoreexcluye.env;.dockerignoreimpide que llegue a la imagen. .env.exampledocumenta cada variable sin valores reales.- La validación es perezosa, así que el build no necesita secretos.
- El endpoint de trabajos usa comparación en tiempo constante del token.
Errores
Sólo se muestran al usuario los errores de dominio, escritos en español. Todo lo demás se registra completo en el servidor y se sustituye por un mensaje genérico: una traza de pila o un error de Prisma en pantalla es a la vez inútil y una filtración.
Lista de verificación previa a producción
Autenticación y sesiones
-
AUTH_SECRETcon 32+ caracteres, generado aleatoriamente - Las cookies llegan con
secure(HTTPS activo) - Desactivar un empleado corta su acceso de inmediato
- Restablecer una contraseña obliga a cambiarla al ingresar
Autorización
- Un empleado no puede abrir el servicio de otro (404)
- Un empleado no ve códigos de acceso fuera de su servicio activo
- Un empleado no ve tarifas, precios ni márgenes en ninguna pantalla
- Las rutas de
/adminrechazan a un empleado
Almacenamiento
- El bucket de R2 es privado
- Una URL firmada sin sus parámetros de firma es rechazada
- El token de R2 tiene permiso sólo sobre el bucket de evidencia
Infraestructura
- PostgreSQL no responde desde fuera de la VPS
- HTTPS con renovación automática
-
/api/jobs/runsin token responde 401 - El contenedor corre como usuario sin privilegios
-
NODE_ENV=production
Datos
- Los respaldos funcionan y se ha probado una restauración
- Ningún secreto en el historial de git
- La semilla no se ha ejecutado en producción
Los tests de integración cubren buena parte de estos puntos de forma
automática: ver tests/integration/authorization.test.ts
y tests/integration/evidence-storage.test.ts.