diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a2deac3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,202 @@ +# 🔒 Reporte de Seguridad del Proyecto Karibeo + +## ⚠️ ADVERTENCIAS CRÍTICAS DE SEGURIDAD + +### 1. CRÍTICO: Verificación de Roles del Cliente (NO SEGURO PARA PRODUCCIÓN) + +**PROBLEMA**: El sistema actual verifica roles de administrador usando el objeto `user` almacenado en localStorage del cliente. + +**Archivos afectados**: +- `src/hooks/useAdminData.ts` (línea 18) +- `src/hooks/useEmergencyData.ts` (línea 16) +- `src/App.tsx` (línea 131) +- `src/pages/SignIn.tsx` (línea 33) + +**Por qué es inseguro**: +```typescript +// ❌ INSEGURO - Un atacante puede modificar localStorage +const isAdmin = user?.role === 'admin'; +``` + +Un atacante puede: +1. Abrir DevTools en el navegador +2. Modificar `localStorage.setItem('karibeo-user', JSON.stringify({...user, role: 'admin'}))` +3. Obtener acceso completo al panel de administración + +**SOLUCIÓN REQUERIDA PARA PRODUCCIÓN**: + +#### Opción A: Backend con verificación de roles (RECOMENDADO) +1. Crear tabla `user_roles` en la base de datos (separada de `users`/`profiles`) +2. Implementar verificación de roles en el backend +3. Cada endpoint de API debe verificar permisos en el servidor +4. Usar Row Level Security (RLS) en Supabase + +```sql +-- Ejemplo de estructura segura +CREATE TABLE user_roles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + role TEXT NOT NULL CHECK (role IN ('admin', 'super_admin', 'user', 'hotel', 'restaurant')), + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, role) +); + +-- Función de seguridad definer (evita recursión RLS) +CREATE OR REPLACE FUNCTION has_role(_user_id UUID, _role TEXT) +RETURNS BOOLEAN +LANGUAGE SQL +STABLE +SECURITY DEFINER +SET search_path = public +AS $$ + SELECT EXISTS ( + SELECT 1 FROM user_roles + WHERE user_id = _user_id AND role = _role + ) +$$; + +-- Política RLS usando la función +CREATE POLICY "Only admins can view all data" +ON some_table +FOR SELECT +TO authenticated +USING (has_role(auth.uid(), 'admin')); +``` + +#### Opción B: Sin Backend +Si NO puedes implementar backend: +1. **Aceptar el riesgo** - La verificación del cliente es solo para UX +2. **No almacenar datos sensibles** - Nada crítico debe depender de roles +3. **Implementar ofuscación** - Dificultar (no prevenir) manipulación +4. **Rate limiting** - Limitar llamadas a API + +### 2. Validación de Inputs Insuficiente + +**PROBLEMA**: La mayoría de formularios no tienen validación robusta. + +**Archivos sin validación**: +- ✅ `src/pages/SignIn.tsx` - **CORREGIDO** +- ✅ `src/pages/SignUp.tsx` - **CORREGIDO** +- ❌ `src/pages/dashboard/AddListing.tsx` +- ❌ `src/pages/dashboard/Settings.tsx` +- ❌ `src/pages/dashboard/Profile.tsx` + +**Riesgos**: +- Inyección SQL (si el backend no valida) +- XSS (Cross-Site Scripting) +- Datos corruptos en la base de datos +- Buffer overflow en campos de texto + +**SOLUCIÓN IMPLEMENTADA**: +- Creado `src/lib/validation.ts` con schemas de validación usando zod +- Actualizado SignIn.tsx con validación completa +- Actualizado SignUp.tsx con validación robusta + +### 3. Credenciales Mock Hardcoded + +**PROBLEMA**: Usuarios de prueba con contraseñas conocidas en `src/contexts/AuthContext.tsx` + +```typescript +// ⚠️ DESARROLLO SOLAMENTE +const mockUsers = { + 'superadmin@karibeo.com': { password: '123456', role: 'super_admin' }, + 'admin@karibeo.com': { password: '123456', role: 'admin' }, + 'user@karibeo.com': { password: '123456', role: 'tourist' } +}; +``` + +**SOLUCIÓN**: +- ✅ Agregada documentación clara de que es solo para desarrollo +- ⚠️ **CRÍTICO**: Eliminar estos usuarios antes de producción +- ⚠️ **CRÍTICO**: Usar variables de entorno para credenciales de prueba + +### 4. Tokens en localStorage + +**PROBLEMA**: Tokens JWT almacenados en localStorage son vulnerables a XSS. + +**Estado actual**: +```typescript +localStorage.setItem('karibeo-token', token); +``` + +**Riesgo**: Si hay una vulnerabilidad XSS, un atacante puede robar tokens. + +**SOLUCIÓN (para implementar)**: +- Usar HttpOnly cookies (requiere backend) +- Implementar refresh tokens con rotación +- Tokens de corta duración (15 minutos) +- Refresh tokens en HttpOnly cookies + +### 5. Sin Rate Limiting + +**PROBLEMA**: No hay protección contra ataques de fuerza bruta en login. + +**SOLUCIÓN REQUERIDA**: +- Implementar rate limiting en el backend +- Bloquear IPs después de N intentos fallidos +- Captcha después de 3 intentos +- Retrasos progresivos entre intentos + +## ✅ Mejoras Implementadas + +1. **Validación de Inputs con Zod** + - Schemas de validación centralizados en `src/lib/validation.ts` + - Validación de email, password, nombres, teléfonos + - Mensajes de error descriptivos + - Protección contra caracteres maliciosos + +2. **Documentación de Seguridad** + - Este documento SECURITY.md + - Comentarios de advertencia en código crítico + - Guías de implementación segura + +3. **Mejora de Formularios de Autenticación** + - SignIn con validación completa + - SignUp con validación de password seguro + - Manejo de errores mejorado + +## 📋 Checklist de Seguridad para Producción + +### ANTES DE DEPLOY: +- [ ] Eliminar usuarios mock de AuthContext.tsx +- [ ] Implementar verificación de roles en el backend +- [ ] Crear tabla user_roles separada +- [ ] Implementar RLS policies en Supabase +- [ ] Migrar tokens a HttpOnly cookies +- [ ] Implementar rate limiting +- [ ] Auditar todos los endpoints de API +- [ ] Habilitar CORS solo para dominios conocidos +- [ ] Configurar CSP (Content Security Policy) +- [ ] Implementar logging de intentos de acceso no autorizado +- [ ] Agregar validación en TODOS los formularios +- [ ] Sanitizar HTML en campos de texto rico +- [ ] Configurar HTTPS obligatorio +- [ ] Implementar 2FA para administradores +- [ ] Revisar dependencias con `npm audit` +- [ ] Configurar variables de entorno adecuadamente +- [ ] Implementar monitoreo de seguridad + +### TESTING DE SEGURIDAD: +- [ ] Pruebas de penetración +- [ ] Análisis de vulnerabilidades XSS +- [ ] Análisis de inyección SQL +- [ ] Pruebas de escalación de privilegios +- [ ] Pruebas de autenticación bypass +- [ ] Pruebas de CSRF + +## 🔗 Recursos Adicionales + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Supabase Security Best Practices](https://supabase.com/docs/guides/auth/row-level-security) +- [JWT Security Best Practices](https://tools.ietf.org/html/rfc8725) +- [Input Validation with Zod](https://zod.dev/) + +## 📞 Contacto de Seguridad + +Si descubres una vulnerabilidad de seguridad, por favor NO la publiques públicamente. Contacta al equipo de desarrollo directamente. + +--- + +**Última actualización**: ${new Date().toISOString().split('T')[0]} +**Versión del documento**: 1.0 +**Estado**: Desarrollo - NO apto para producción sin correcciones diff --git a/src/App.tsx b/src/App.tsx index a168b61..498f1d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -125,6 +125,7 @@ const DashboardGate = () => { ); } + // ⚠️ SECURITY WARNING: Client-side role check - see SECURITY.md const role = (user as any)?.role; console.log('🚪 DashboardGate - checking role:', role, 'isAdmin?', (role === 'admin' || role === 'super_admin')); diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index eafd42f..fcb53e9 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -74,7 +74,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children try { console.log('Login attempt with:', { email, password: '***' }); - // Mock users for testing + // ⚠️ SECURITY WARNING: Mock users for DEVELOPMENT ONLY + // ⚠️ REMOVE BEFORE PRODUCTION DEPLOYMENT + // These hardcoded credentials are a CRITICAL security vulnerability in production const mockUsers = { 'superadmin@karibeo.com': { id: '1', diff --git a/src/hooks/useAdminData.ts b/src/hooks/useAdminData.ts index 6ef9192..701998f 100644 --- a/src/hooks/useAdminData.ts +++ b/src/hooks/useAdminData.ts @@ -14,7 +14,13 @@ export const useAdminData = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Check if user has admin permissions + // ⚠️ CRITICAL SECURITY WARNING: CLIENT-SIDE ROLE VERIFICATION + // This checks roles from localStorage which can be easily manipulated by attackers + // For PRODUCTION, you MUST implement server-side role verification: + // 1. Create a separate 'user_roles' table in database + // 2. Verify roles on EVERY API endpoint in the backend + // 3. Use Row Level Security (RLS) policies in Supabase + // This client-side check is ONLY for UI/UX purposes, NOT for actual security const isAdmin = user?.role === 'admin' || user?.role === 'super_admin'; const isSuperAdmin = user?.role === 'super_admin'; diff --git a/src/hooks/useEmergencyData.ts b/src/hooks/useEmergencyData.ts index b0306f3..6cc0e9d 100644 --- a/src/hooks/useEmergencyData.ts +++ b/src/hooks/useEmergencyData.ts @@ -11,7 +11,10 @@ export const useEmergencyData = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Check if user has emergency permissions + // ⚠️ CRITICAL SECURITY WARNING: CLIENT-SIDE ROLE VERIFICATION + // This checks roles from localStorage which can be manipulated by users + // For PRODUCTION: Implement server-side verification with user_roles table and RLS + // This is ONLY for UI/UX, NOT for actual security const isOfficer = user?.role === 'politur' || user?.role === 'admin' || user?.role === 'super_admin'; const isAdmin = user?.role === 'admin' || user?.role === 'super_admin'; diff --git a/src/lib/validation.ts b/src/lib/validation.ts new file mode 100644 index 0000000..6af7768 --- /dev/null +++ b/src/lib/validation.ts @@ -0,0 +1,133 @@ +import { z } from 'zod'; + +/** + * SECURITY: Input validation schemas using zod + * All user inputs must be validated to prevent injection attacks + */ + +// Authentication schemas +export const loginSchema = z.object({ + email: z + .string() + .trim() + .min(1, 'El email es requerido') + .email('Email inválido') + .max(255, 'Email demasiado largo'), + password: z + .string() + .min(6, 'La contraseña debe tener al menos 6 caracteres') + .max(128, 'Contraseña demasiado larga'), +}); + +export const registerSchema = z.object({ + fullName: z + .string() + .trim() + .min(2, 'El nombre debe tener al menos 2 caracteres') + .max(100, 'Nombre demasiado largo') + .regex(/^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$/, 'El nombre solo puede contener letras'), + email: z + .string() + .trim() + .min(1, 'El email es requerido') + .email('Email inválido') + .max(255, 'Email demasiado largo'), + password: z + .string() + .min(8, 'La contraseña debe tener al menos 8 caracteres') + .max(128, 'Contraseña demasiado larga') + .regex( + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, + 'La contraseña debe contener al menos una mayúscula, una minúscula y un número' + ), + confirmPassword: z.string(), + userType: z.enum(['tourist', 'business'], { + errorMap: () => ({ message: 'Tipo de usuario inválido' }), + }), +}).refine((data) => data.password === data.confirmPassword, { + message: 'Las contraseñas no coinciden', + path: ['confirmPassword'], +}); + +// User profile update schema +export const profileUpdateSchema = z.object({ + name: z + .string() + .trim() + .min(2, 'El nombre debe tener al menos 2 caracteres') + .max(100, 'Nombre demasiado largo') + .regex(/^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$/, 'El nombre solo puede contener letras') + .optional(), + phone: z + .string() + .trim() + .regex(/^\+?[0-9\s\-()]+$/, 'Número de teléfono inválido') + .max(20, 'Número demasiado largo') + .optional(), + address: z + .string() + .trim() + .max(500, 'Dirección demasiado larga') + .optional(), +}); + +// Contact form schema +export const contactFormSchema = z.object({ + name: z + .string() + .trim() + .min(2, 'El nombre debe tener al menos 2 caracteres') + .max(100, 'Nombre demasiado largo'), + email: z + .string() + .trim() + .email('Email inválido') + .max(255, 'Email demasiado largo'), + phone: z + .string() + .trim() + .regex(/^\+?[0-9\s\-()]+$/, 'Número de teléfono inválido') + .max(20, 'Número demasiado largo') + .optional(), + message: z + .string() + .trim() + .min(10, 'El mensaje debe tener al menos 10 caracteres') + .max(2000, 'Mensaje demasiado largo'), +}); + +// Review schema +export const reviewSchema = z.object({ + rating: z + .number() + .min(1, 'La calificación mínima es 1') + .max(5, 'La calificación máxima es 5') + .int('La calificación debe ser un número entero'), + comment: z + .string() + .trim() + .min(10, 'El comentario debe tener al menos 10 caracteres') + .max(1000, 'Comentario demasiado largo') + .optional(), +}); + +// Search query schema +export const searchSchema = z.object({ + query: z + .string() + .trim() + .min(1, 'La búsqueda no puede estar vacía') + .max(200, 'Búsqueda demasiado larga'), + category: z + .string() + .trim() + .max(50, 'Categoría inválida') + .optional(), +}); + +export type LoginInput = z.infer; +export type RegisterInput = z.infer; +export type ProfileUpdateInput = z.infer; +export type ContactFormInput = z.infer; +export type ReviewInput = z.infer; +export type SearchInput = z.infer; diff --git a/src/pages/SignIn.tsx b/src/pages/SignIn.tsx index f5a4ae3..8836283 100644 --- a/src/pages/SignIn.tsx +++ b/src/pages/SignIn.tsx @@ -6,6 +6,7 @@ import { useAuth } from '@/contexts/AuthContext'; import { useLanguage } from '@/contexts/LanguageContext'; import { Apple, Eye, EyeOff } from 'lucide-react'; import { FaGoogle } from 'react-icons/fa'; +import { loginSchema, type LoginInput } from '@/lib/validation'; const SignIn = () => { const [email, setEmail] = useState(''); @@ -24,8 +25,10 @@ const SignIn = () => { setIsLoading(true); setError(''); + // SECURITY: Validate inputs before processing try { - await login(email, password); + const validatedData = loginSchema.parse({ email, password }); + await login(validatedData.email, validatedData.password); // Decide destination based on role const cached = localStorage.getItem('karibeo-user'); const u = user ?? (cached ? JSON.parse(cached) : null); @@ -36,7 +39,12 @@ const SignIn = () => { navigate('/dashboard'); } } catch (err: any) { - setError(err.message); + // Handle validation errors from zod + if (err.name === 'ZodError') { + setError(err.errors[0]?.message || 'Datos inválidos'); + } else { + setError(err.message); + } } finally { setIsLoading(false); } diff --git a/src/pages/SignUp.tsx b/src/pages/SignUp.tsx index 27eb4db..1f819af 100644 --- a/src/pages/SignUp.tsx +++ b/src/pages/SignUp.tsx @@ -6,6 +6,7 @@ import { useAuth } from '@/contexts/AuthContext'; import { useLanguage } from '@/contexts/LanguageContext'; import { Apple, Eye, EyeOff } from 'lucide-react'; import { FaGoogle } from 'react-icons/fa'; +import { registerSchema, type RegisterInput } from '@/lib/validation'; const SignUp = () => { const [formData, setFormData] = useState({ @@ -36,19 +37,17 @@ const SignUp = () => { e.preventDefault(); setError(''); - if (formData.password !== formData.confirmPassword) { - setError('Las contraseñas no coinciden'); - return; - } - if (!agreeToTerms) { setError('Debes aceptar los términos de servicio'); return; } - setIsLoading(true); - + // SECURITY: Validate all inputs before processing try { + const validatedData = registerSchema.parse(formData); + + setIsLoading(true); + await register({ name: formData.fullName, email: formData.email, @@ -59,7 +58,12 @@ const SignUp = () => { }); navigate('/dashboard'); } catch (err: any) { - setError(err.message); + // Handle validation errors from zod + if (err.name === 'ZodError') { + setError(err.errors[0]?.message || 'Datos inválidos'); + } else { + setError(err.message); + } } finally { setIsLoading(false); }