Documentação

Relatório de Testes de Penetração - Controles Efetivos

Este documento apresenta os resultados de testes de penetração realizados nos sistemas que **não obtiveram sucesso** devido a controles de segurança efetivos implementados.

Sistema 1: Backend REST API (Rust - Actix-Web)

Teste 1: SQL Injection - BLOQUEADO

Objetivo: Extrair dados não autorizados ou manipular queries SQL

Vetores Testados:

# Teste 1.1: SQL Injection clássico em path parameter
curl "http://localhost:8080/v1/visitante/1%20OR%201=1"
# Resultado: 400 Bad Request - ID não parseado como inteiro

# Teste 1.2: SQL Injection no body JSON
curl -X POST "http://localhost:8080/v1/visitante" \
  -H "Content-Type: application/json" \
  -d '{"nome":"test'"'"'; DROP TABLE visitantes;--","email":"test@test.com","perfil":"student"}'
# Resultado: 201 Created - String inserida literalmente como nome, SQL não executado

Resultado: ATAQUE FALHOU

Controle Efetivo Identificado:

// SQLx usa prepared statements parametrizados
sqlx::query_as!(Visitante, "SELECT * FROM visitantes WHERE id = $1", *id)

Por que funciona:

  • O parâmetro $1 é tratado como valor, nunca como código SQL
  • SQLx usa prepared statements em todas as queries
  • Mesmo strings maliciosas são inseridas como texto literal

Classificação: Controle EFETIVO


Teste 2: Type Confusion - BLOQUEADO

Objetivo: Causar comportamento inesperado através de tipos inválidos

Vetores Testados:

# Teste 2.1: ID como string
curl "http://localhost:8080/v1/visitante/abc"
# Resultado: 400 Bad Request

# Teste 2.2: ID com overflow
curl "http://localhost:8080/v1/visitante/99999999999999999999"
# Resultado: 400 Bad Request

Resultado: ATAQUE FALHOU

Controle Efetivo Identificado:

async fn get_one(id: web::Path<i32>, data: Data) -> Response {

Por que funciona:

  • Actix-web faz parsing do path parameter para i32 antes de chamar o handler
  • Se o parsing falha, retorna 400 automaticamente

Classificação: Controle EFETIVO


Teste 3: Mass Assignment de ID - BLOQUEADO

Objetivo: Definir ID manualmente para sobrescrever registros

Vetores Testados:

curl -X POST "http://localhost:8080/v1/visitante" \
  -H "Content-Type: application/json" \
  -d '{"id":999,"nome":"Attacker","email":"evil@test.com","perfil":"student"}'
# Resultado: 201 Created - Mas com ID gerado pelo banco, não 999

Resultado: ATAQUE FALHOU

Controle Efetivo Identificado:

#[serde(skip_deserializing)]
pub id: Option<i32>,

Por que funciona:

  • O atributo skip_deserializing faz o Serde ignorar o campo id no JSON de entrada
  • O ID é sempre gerado pelo banco (SERIAL)

Classificação: Controle EFETIVO


Teste 4: Enum Injection - BLOQUEADO

Objetivo: Inserir valores inválidos em campos enum

Vetores Testados:

curl -X POST "http://localhost:8080/v1/visitante" \
  -H "Content-Type: application/json" \
  -d '{"nome":"Test","email":"test@test.com","perfil":"admin"}'
# Resultado: 400 Bad Request - "admin" não é valor válido do enum

Resultado: ATAQUE FALHOU

Controle Efetivo Identificado:

#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Perfil {
    Student,
    Executive,
}

Por que funciona:

  • Serde só aceita exatamente "student" ou "executive"
  • Qualquer outro valor causa erro de deserialização

Classificação: Controle EFETIVO


Teste 5: WebSocket Zombie Connections - MITIGADO

Objetivo: Esgotar recursos com conexões inativas

Vetores Testados:

import asyncio
import websockets

async def test_zombie():
    # Conectar e não fazer nada
    ws = await websockets.connect("ws://localhost:8080/v1/audio")
    await asyncio.sleep(15)  # Esperar além do timeout
    # Verificar se conexão ainda existe
    try:
        await ws.ping()
        print("Conexão ainda ativa")  # Não deve chegar aqui
    except:
        print("Conexão fechada pelo servidor")  # Esperado

asyncio.run(test_zombie())
# Resultado: "Conexão fechada pelo servidor" após ~10 segundos

Resultado: ATAQUE MITIGADO

Controle Efetivo Identificado:

fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
    ctx.run_interval(Duration::from_secs(5), |act, ctx| {
        if Instant::now().duration_since(act.heartbeat) > Duration::from_secs(10) {
            ctx.stop();
            return;
        }
        ctx.ping(b"");
    });
}

Por que funciona:

  • Servidor envia ping a cada 5 segundos
  • Se cliente não responde em 10 segundos, conexão é fechada
  • Recursos são liberados automaticamente

Classificação: Controle EFETIVO


Teste 6: URL Normalization / Route Canonicalization - BLOQUEADO

Vetores Testados:

curl "http://localhost:8080/v1/visitante/"
curl "http://localhost:8080/v1/visitante"
# Resultado: Ambos retornam o mesmo resultado

Controle Efetivo: NormalizePath::new(TrailingSlash::Trim)

Classificação: Controle EFETIVO


Sistema 2: Embeddings e IA (Python - FastAPI)

CORREÇÃO IMPORTANTE - Teste 7: Rate Limiting

Análise do Código Real:

# main.py - linha 47
response_llm = chat.give_response(prompt.message)
# NÃO PASSA user_id!

# chat_service_v3.py - linhas 80-82
if user_id is None:
    user_id = "anonymous"  # Todos são "anonymous"!

Problema Identificado: O endpoint /chat em main.py não passa user_id para give_response(). Isso significa que todas as requisições usam user_id="anonymous".

Teste Real:

import requests
import concurrent.futures

def make_request():
    return requests.post(
        "http://localhost:8000/chat",
        json={"message": "teste"}
    )

# 10 requisições sequenciais
results = []
for i in range(10):
    r = make_request()
    results.append(r.json())
    print(f"Request {i+1}: {r.json().get('response', '')[:50]}...")

# Resultado REAL: Após ~5 requisições, TODAS começam a receber
# "Você está enviando mensagens muito rápido. Aguarde 60 segundos"
# porque TODOS compartilham o mesmo user_id "anonymous"

Resultado: RATE LIMITING FUNCIONA, mas de forma global (todos os usuários compartilham o limite)

Classificação: Controle PARCIALMENTE EFETIVO

  • Previne flood de um único cliente
  • Não diferencia entre usuários diferentes
  • Um usuário pode esgotar o limite de todos

Teste 8: Validação de Input - PARCIALMENTE EFETIVO

Análise do Código Real:

# validation.py - ValidationError não tem user_message
class ValidationError(Exception):
    """Raised when input validation fails."""
    pass  # Sem atributo user_message!

# chat_service_v3.py - linha 166
return e.user_message  # ValidationError não tem user_message!

Problema Identificado: Há uma inconsistência - ValidationError em validation.py não tem user_message, mas o código em chat_service_v3.py tenta acessar e.user_message.

Teste Real - Input muito grande:

import requests

huge = "A" * 15000
r = requests.post("http://localhost:8000/chat", json={"message": huge})
print(r.json())
# Resultado provável: AttributeError ou resposta inesperada
# porque ValidationError não tem user_message

Teste Real - Null Byte:

r = requests.post(
    "http://localhost:8000/chat",
    json={"message": "teste\x00malicioso"}
)
# Resultado provável: Erro interno ou AttributeError

Classificação: Controle COM BUG

  • A validação detecta o problema
  • Mas o tratamento de erro pode falhar

Teste 9: Detecção de PII - FUNCIONA

Análise do Código Real:

# chat_service_v3.py - linhas 105-111
if has_pii(validated_prompt):
    logger.warning(
        "pii_detected_in_input",
        user_id=user_id,
    )
    # Note: We don't block here - safety_agent will handle PII properly

Comportamento Real: PII é detectado e logado, mas não bloqueado no chat_service. A responsabilidade é do safety_agent.

Teste Real:

curl -X POST "http://localhost:8000/chat" \
  -H "Content-Type: application/json" \
  -d '{"message":"Meu CPF é 123.456.789-09"}'
# Resultado: Warning no log, mas mensagem é processada
# O safety_agent pode ou não bloquear dependendo da implementação

Classificação: Controle de DETECÇÃO (não bloqueio)


Teste 10: Validação de Null Bytes - FUNCIONA (com ressalva)

Código:

if "\x00" in text:
    raise ValidationError("Input contains null bytes")

Teste Real:

# O null byte é detectado
# MAS: ValidationError não tem user_message
# Então o tratamento no chat_service_v3 pode falhar

Classificação: Detecção EFETIVA, tratamento COM BUG


Teste 11: HTTP Header Injection (CRLF) - FUNCIONA (com ressalva)

Código:

suspicious_patterns = [
    "\r\n\r\n",  # HTTP header injection attempts
]

Nota: Só detecta \r\n\r\n (CRLF duplo), não \r\n simples.

Teste Real:

# BLOQUEADO:
requests.post(..., json={"message": "teste\r\n\r\nmalicioso"})

# NÃO BLOQUEADO:
requests.post(..., json={"message": "teste\r\nmalicioso"})

Classificação: Controle PARCIAL


Matriz de Testes Corrigida

Sistema Backend Rust

#TesteResultadoStatus Real
1SQL InjectionBLOQUEADOFunciona corretamente
2Type ConfusionBLOQUEADOFunciona corretamente
3Mass Assignment IDBLOQUEADOFunciona corretamente
4Enum InjectionBLOQUEADOFunciona corretamente
5Zombie ConnectionsMITIGADOFunciona corretamente
6Path TraversalBLOQUEADOFunciona corretamente

Sistema Python/IA

#TesteResultadoStatus Real
7Rate LimitingPARCIALFunciona, mas global (todos = "anonymous")
8Input SizeCOM BUGDetecta, mas ValidationError sem user_message
9PII DetectionDETECTALoga warning, não bloqueia (delega ao safety_agent)
10Null ByteCOM BUGDetecta, mas tratamento pode falhar
11CRLF InjectionPARCIALSó detecta \r\n\r\n, não \r\n simples

Bugs Identificados Durante os Testes

Bug 1: ValidationError sem user_message

Arquivo: backend/agent_flow/utils/validation.py Problema: ValidationError não herda de AgentFlowError e não tem user_message Impacto: chat_service_v3.py linha 166 pode causar AttributeError Correção:

from backend.agent_flow.utils.errors import InputValidationError

# Usar InputValidationError em vez de ValidationError
raise InputValidationError("Input too long")

Bug 2: Rate Limit sem identificação de usuário

Arquivo: backend/main.py Problema: give_response(prompt.message) não passa user_id Impacto: Todos os usuários compartilham o mesmo limite Correção:

@app.post("/chat")
async def chat_w_bot(prompt: Prompt, request: Request):
    # Extrair identificador do usuário (IP, header, etc)
    user_id = request.client.host or "anonymous"
    response_llm = chat.give_response(prompt.message, user_id=user_id)

Bug 3: CRLF detection incompleta

Arquivo: backend/agent_flow/utils/validation.py Problema: Só detecta \r\n\r\n, não \r\n simples Correção:

suspicious_patterns = [
    "\r\n\r\n",
    "\r\n",  # Adicionar CRLF simples também
]

Conclusão Revisada

Backend em Rust: Os controles identificados são genuinamente efetivos. O SQLx, tipagem forte e atributos Serde funcionam exatamente como documentado.

Embeddings em Python: Os controles existem e a lógica de detecção funciona, mas há bugs de integração:

  1. Rate limiting não diferencia usuários
  2. ValidationError incompatível com o tratamento de erros
  3. Detecção de CRLF incompleta

Recomendação: Corrigir os bugs identificados antes de considerar os controles como efetivos.