Gracias por ayudar a mantener segura esta aplicación.
No abras un issue público para problemas de seguridad. Usa el reporte privado de GitHub:
- Entra en https://github.com/jaervic/budgetify/security/advisories/new
- Describe el problema, cómo reproducirlo y su impacto.
Recibirás respuesta lo antes posible. Una vez corregido, se publicará el aviso.
Por defecto esta aplicación no tiene backend, ni cuentas, ni sesiones, ni servidores. Todo el cálculo y el almacenamiento ocurren en el navegador de cada persona:
- Los datos (borrador, catálogo, datos de la clínica) se guardan en el
localStoragedel propio navegador. No se envían a ningún servidor. - La única conexión externa es un
GETa la API pública de la tasa del BCV (https://ve.dolarapi.com), que no lleva datos del usuario. - No hay analítica, rastreadores ni cookies de terceros.
La app permite activar una cuenta para sincronizar entre equipos. Es una función opt-in: si nadie inicia sesión, el modelo anterior no cambia. Cuando se activa:
- La identidad se gestiona con Supabase Auth. Se puede entrar con correo y
contraseña o con un enlace mágico por correo. La contraseña la almacena Supabase
como hash; la app nunca la guarda ni la escribe en
localStorage. - Los correos de acceso y de recuperación salen por el SMTP configurado en Supabase (por ejemplo, Gmail). Las credenciales SMTP viven solo en Supabase, nunca en el cliente.
- Los datos viajan por HTTPS y Supabase los guarda cifrados en reposo.
- Cada fila de
profiles,documentsydraftsestá protegida con Row Level Security (RLS):auth.uid() = user_id. Un usuario autenticado no puede leer, insertar, actualizar ni borrar filas de otro. - El cliente usa solo la clave anónima de Supabase. La
service_rolevive únicamente en la Edge Functiondelete-account. - No es cifrado de extremo a extremo: el proveedor puede acceder técnicamente a los datos. La privacidad se apoya en el aislamiento por usuario, el cifrado gestionado y el control de acceso, no en una clave que solo tenga el usuario.
- El token de sesión de Supabase se guarda en
localStorage(como el resto del estado), así que un XSS sigue siendo el riesgo principal. La CSP y la ausencia dedangerouslySetInnerHTMLlo mitigan.
- XSS (que un script inyectado pudiera leer el
localStorageo la sesión). - CSRF/clickjacking (mitigado con CSP
frame-ancestorsyX-Frame-Options). - Dependencias comprometidas o desactualizadas.
- Fallo de RLS o de la Edge Function que permita acceder a datos de otra cuenta.
- Filtración de la
service_role(nunca debe estar en el bundle del cliente). - Fuerza bruta o credential stuffing contra el login con contraseña (mitigado con límites de intentos, longitud mínima y, opcionalmente, CAPTCHA de Supabase).
- Robo de la contraseña por reutilización en otros sitios; recomienda una contraseña única y un gestor de contraseñas.
- Content-Security-Policy estricta sin
unsafe-inlineniunsafe-evalen scripts (ververcel.json). Solo se permite WebAssembly ('wasm-unsafe-eval'), que necesita la librería que arma el PDF,data:enconnect-srcporque esa misma librería carga su módulo desde una URL de datos (inerte: no puede salir a la red), yhttps://*.supabase.co/wss://*.supabase.copara la cuenta opcional. - Cabeceras
HSTS,X-Content-Type-Options,Referrer-Policy,X-Frame-Options,Permissions-Policy,Cross-Origin-Opener-PolicyyCross-Origin-Resource-Policy. - Validación con Zod de todo archivo importado y de todo dato que llega de la nube,
con límites de tamaño y longitud. Las imágenes solo se aceptan como
data:image/(png|jpeg|webp);base64,…. - Contraseñas: mínimo 8 caracteres en la app, política de Supabase Auth y límites de intentos. La recuperación se hace con un enlace enviado por el SMTP configurado. Si se desactiva la confirmación por correo, el correo funciona como identificador y no como prueba de propiedad; el acceso sigue protegido por la contraseña y RLS.
- RLS por usuario y límites de tamaño en la base de datos, con una guía de
verificación con dos cuentas en
supabase/README.md. - Sin
eval, sinnew Functiony sindangerouslySetInnerHTML. npm audit --audit-level=highen el CI y actualizaciones automáticas con Dependabot.- Borrado de cuenta desde la app: elimina el usuario de Auth y, por cascada, sus perfiles, documentos y borradores. El borrado local es independiente.
Como los datos viven en el navegador, en una computadora compartida otra persona que use el mismo perfil del navegador podría ver el borrador. Si además hay una cuenta:
- Al cerrar sesión, marca «Borrar también los datos guardados en este equipo».
- Si otra persona inicia sesión en un equipo que tenía datos de una cuenta anterior, la app no los sube automáticamente: pregunta si combinarlos o descartarlos.
- Si no usas cuenta, pulsa Limpiar al terminar o abre la app en una ventana de incógnito.
La demo en budgetify-silk.vercel.app se despliega con VITE_SYNC_ENABLED=false: no
muestra la cuenta ni envía datos a Supabase. No introduzcas datos reales de pacientes en
la demo. Para usar la sincronización, despliega tu propia instancia siguiendo
la documentación técnica y adapta la
plantilla de política de privacidad.