Este documento explica las mitigaciones de seguridad implementadas, sus limitaciones intencionales para hackathon, y qué se haría diferente en producción real.
Themis se demuestra públicamente sobre HTTP en una VPS Vultr sin autenticación. Esto es una decisión consciente para que el jurado pueda explorar libremente. La URL no se publica fuera del jurado durante el evento.
Threat model:
- Aceptamos: jurado curioso explorando endpoints
- Aceptamos: análisis de tráfico HTTP local
- Mitigamos: abuso del API que cueste plata (Claude, Browserbase, ElevenLabs)
- Mitigamos: SSRF a metadata services / private CIDRs
- Mitigamos: leak de API keys via UI
- NO mitigamos: ataques sofisticados, MITM en HTTP, ofensivas post-pitch
Helpers reusables que todos los endpoints sensibles importan:
| Helper | Propósito |
|---|---|
validateStartUrl(url) |
Allowlist + blocklist anti-SSRF |
isBlockedHost(host) |
Bloquea localhost, 169.254.169.254, RFC1918, .internal, .local |
allowedSourceHosts() |
Lee SSRF_ALLOWED_HOSTS env + URLs configuradas |
LIMITS.* |
Caps de bytes/chars por endpoint (proteger billing) |
jsonTooBig(raw, max) |
Check body size pre-parse |
rateLimit(key, max, ms) |
In-memory por IP, best-effort |
getClientIp(req) |
Lee X-Forwarded-For / X-Real-IP |
sanitizedError(err, fallback) |
En prod no leak stack/paths |
badRequest / tooLarge / tooManyRequests |
Responses estándar |
| Endpoint | Validaciones |
|---|---|
POST /api/browser/session |
SSRF allowlist + rate limit (3/min) + sanitized errors |
GET /api/browser/observe |
sessionId pattern + rate limit (90/min para polling) |
POST /api/browser/finalize |
sessionId pattern + audioTranscript cap + rate limit (10/min) |
POST /api/voice |
text cap 1500 chars + voice_id pattern + rate limit (30/min) |
POST /api/whisper |
file size 4MB + MIME type check + rate limit (15/min) |
POST /api/playbook/extract |
body 256KB cap + rate limit (10/min) |
POST /api/execute |
body 128KB + max 200 steps + rate limit (5/min) |
POST /api/playbooks |
body 128KB + max 200 steps + name length + rate limit (10/min) |
GET /api/playbooks |
rate limit (60/min) |
POST /api/recommendations |
rate limit (20/min) + input validation |
POST /api/skus (erp-destino) |
body 32KB + max 30 fields + field length 500 + rate limit (30/min) |
Antes en apps/operator-ui/src/app/diagnostics/page.tsx se mostraban los
primeros 12 chars del ANTHROPIC_API_KEY y los primeros 8 del
BROWSERBASE_PROJECT_ID. Se quitó. Ahora solo dice "Key configurada"
sin leak.
- El secret key NUNCA aparece en respuestas API ni logs.
/api/solana/healthy/api/statussolo retornan elwalletAddress()(público).createSolanaClientFromEnv()lee el secret SOLO server-side.
docker-compose.yml usa env_file: ./.env.production para inyectar secrets
en runtime, NO ENV en los Dockerfiles (que quedaría bakeado en el layer).
.env.production NO está en el repo (cubierto por .gitignore con .env*).
Decisión: el jurado debe poder usar /teach y /execute sin login.
Mitigación parcial: rate limits + body caps. Si alguien encuentra la URL
y abusa, el daño económico está topado (ej. ElevenLabs 30 req/min × 1500 chars
= ~$0.05/min).
Cómo lo arreglaríamos en prod: NextAuth con magic links + middleware
opcional con header X-Demo-Key que el jurado conoce.
Se resetean al restart del server. En un horizontal scale serían inefectivos. Producción: Upstash Redis o Vercel KV.
La SSE puede ser polleada por un atacante para saber qué está ejecutando otro usuario. Mitigación: cada llamada crea su propia ejecución y stream (no es broadcast — el atacante necesitaría un sessionId válido para hijack).
Acepta reusar una sesión Browserbase ya creada (para el flow observación→ejecución).
Un atacante con sessionId válido podría hijack y ejecutar acciones costosas.
Mitigación: sessionId pattern-validated + sesiones se cierran después de
finalize/error. Window de ataque ~5 min máx.
Tradeoff explícito: HTTPS requiere dominio + Let's Encrypt → 30+ min de setup. Para devnet demo de hackathon es aceptable. Producción: Caddy con DNS provider integrado.
console.error("[endpoint]", err) loggea el error completo. Si alguien
gana acceso a la VPS, puede leer logs con detalles internos.
Mitigación: logs solo accesibles vía SSH a root@<vultr-ip>.
SSRF_ALLOWED_HOSTS env permite agregar hosts. Si alguien con acceso al
.env.production agrega un host malicioso → puede SSRF.
Mitigación: solo el deploy tiene acceso al .env.production.
Antes de hacer docker compose up -d:
-
.env.productionNO está commiteado al repo -
.gitignorecubre.env*(verificado) -
NODE_ENV=productionestá en.env.production -
SSRF_ALLOWED_HOSTSconfigurado con los hosts que Browserbase puede tocar - Firewall Vultr abierto solo en 22, 3000, 3001, 3002
- No exponer otros puertos por accidente
-
SOLANA_WALLET_SECRET_KEYsolo tiene SOL en devnet (cero valor real) -
MONGODB_URIapunta a un cluster dedicado al demo (no compartido con otros proyectos)
- Destruir VPS Vultr (
Vultr panel → Destroy) - Rotar API keys: Anthropic, Browserbase, ElevenLabs, OpenAI, Gemini, MongoDB
- Mover SOL restante de la wallet devnet a otra (o ignorar — es devnet)
- Sacar el tunnel cloudflared si quedó arriba
Para hackathon: avisar a Jhulyam directamente. Post-hackathon: este repo se archiva; abrir issue público en GitHub.