← Todos os posts

Uma API uncensored para red-teaming

20 ago 2026 · 6 min de leitura · red-teaming, security

Existe uma classe de trabalho em que a recusa de um modelo não é um recurso de segurança: é um bug silencioso de medição. Se o modelo que você usa como ferramenta de análise responde "não posso ajudar com isso", o seu script não recebe um erro. Ele recebe uma string. E essa string entra na sua planilha de resultados como se fosse um dado.

Esse é o problema central, e é de engenharia antes de ser de política.

A recusa é um falso negativo

Considere um harness de red-teaming que gera prompts adversariais contra a sua própria aplicação, manda cada um para o alvo e classifica a resposta. Se o gerador recusa metade dos casos, a sua cobertura de teste deixa de ser função da superfície de ataque do seu app e passa a ser função do alinhamento do fornecedor do gerador. Você não testou o que achou que testou. Pior: o relatório final diz "0 vulnerabilidades encontradas" quando o que aconteceu foi "0 tentativas realizadas". Os dois resultados são indistinguíveis a jusante.

O mesmo padrão aparece em quatro lugares:

Avaliação de jailbreak e robustez. O objetivo literal da medição é a taxa de recusa do alvo. Se o gerador de ataques ou o modelo-juiz também recusam, você está medindo o filtro de dois fornecedores empilhados, não a robustez do seu sistema. O instrumento contamina a leitura.

Análise de malware e triagem. Colar uma amostra decompilada ou um script ofuscado num modelo mainstream frequentemente dispara recusa no input — o modelo se nega a explicar o que a amostra faz. Isso é trabalho defensivo, feito por quem responde ao incidente, e a recusa quebra o triage exatamente no momento em que ele importa.

Detecção de conteúdo. Para treinar ou avaliar um classificador de abuso, phishing ou fraude, você precisa de exemplos rotulados e de um modelo disposto a analisar o material que está tentando detectar. Um detector que não pode olhar para aquilo que detecta é uma contradição operacional.

Pesquisa. Medir quanto um guardrail custa em capacidade exige um baseline sem guardrail. Sem ele, não existe o eixo de comparação.

Como as recusas são silenciosas, as equipes acabam escrevendo detectores de recusa — o que é, por si só, um problema sem solução:

python
REFUSAL_MARKERS = ("i can't", "i cannot", "i'm unable", "i won't", "as an ai")

def looks_like_refusal(text: str) -> bool:
    """Substring heuristic. Good enough to alarm on, not good enough to trust:
    it misses polite deflections that never use the phrase, and it fires on any
    completion that quotes a refusal. Both error directions corrupt a dataset."""
    return any(m in text[:200].lower() for m in REFUSAL_MARKERS)

Construir um detector pouco confiável para um problema que você poderia eliminar na origem é a troca errada.

O controle está na camada errada

O argumento não é que filtro nenhum deva existir. É que um filtro embutido no peso do fornecedor é o pior lugar possível para ele estar, por três motivos: ele é invisível (você não sabe o que foi recusado nem por quê), não é configurável (não há dial), e não gera log (você não consegue auditar a própria taxa de bloqueio).

Uma política que vive na sua camada tem as três propriedades opostas. Por isso a unbleep expõe o parâmetro policy em vez de decidir por você:

Para pipelines de avaliação, research costuma ser o modo certo — você mantém o dado e ganha a segmentação no histórico. O off é o baseline de comparação.

O parâmetro é opcional e vai por requisição, via extra_body — a governança roda do nosso lado, a cada chamada:

python
resp = client.chat.completions.create(
    model="unbleep",
    messages=[...],
    extra_body={"policy": "research"},  # recorded on the usage row; answers exactly like off
)

Na prática

A API fala Chat Completions. Você aponta o SDK da OpenAI para https://unbleep.ai/v1, troca a chave, e o resto do seu código continua igual.

O detalhe que vale codificar explicitamente: trate a recusa como um estado próprio, nunca como uma resposta vazia. Mesmo num modelo uncensored, um caso-limite pode voltar recusa, e você quer que isso apareça como refused no seu relatório e não como um "pass" silencioso.

python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://unbleep.ai/v1",
    api_key=os.environ["UNBLEEP_API_KEY"],
)

# A refusal from the analyst model is a FALSE NEGATIVE: downstream it is
# indistinguishable from "nothing found". Score it as its own state.
REFUSAL_MARKERS = (
    "i can't help", "i cannot help", "i'm unable to",
    "i can't assist", "as an ai", "i won't",
)


def triage(sample: str) -> dict:
    """Summarise what a suspicious artefact does. Defensive analysis only."""
    resp = client.chat.completions.create(
        model="unbleep",
        messages=[
            {
                "role": "system",
                "content": "You are a malware triage analyst. Describe observed "
                           "behaviour, persistence and IOCs. Be terse and factual.",
            },
            {"role": "user", "content": sample},
        ],
        temperature=0.0,      # deterministic: this output feeds a report
        max_tokens=512,
        extra_body={"policy": "research"},   # answers like off; the level is recorded on the usage row
    )
    text = resp.choices[0].message.content or ""
    low = text.lower()
    return {
        "refused": any(m in low for m in REFUSAL_MARKERS),
        "report": text,
        "tokens": resp.usage.total_tokens,
    }


if __name__ == "__main__":
    out = triage("powershell -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAA...")
    print("REFUSED" if out["refused"] else out["report"])

Note o extra_body: policy não faz parte do schema da OpenAI, então o SDK precisa dele por ali para chegar ao endpoint sem ser descartado na serialização.

O mesmo em curl, útil para um smoke test antes de plugar no harness:

bash
curl https://unbleep.ai/v1/chat/completions \
  -H "Authorization: Bearer $UNBLEEP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "unbleep",
    "messages": [
      {"role": "system", "content": "You are a security analyst. Be terse."},
      {"role": "user", "content": "Classify this URL and name the signals that decide it: hxxps://account-verify.example[.]com/login?next=https://real-bank.example"}
    ],
    "policy": "research",
    "temperature": 0
  }'

Os códigos de erro seguem o envelope da OpenAI, então o seu tratamento de exceções existente já funciona: 401 chave inválida, 402 crédito esgotado, 422 bloqueado por policy: strict, 429 rate limit, 5xx upstream (seguro re-tentar com backoff). Os headers x-ratelimit-* vêm em toda resposta, para você regular o ritmo do harness sem chutar.

Uma nota sobre retenção, já que pipelines de segurança lidam com material sensível por definição: nós armazenamos o corpo da requisição, o conteúdo da resposta e o IP de origem de toda chamada que aceitamos e encaminhamos a um modelo, por 30 dias, para investigação de abuso, suporte e disputas de cobrança. Os corpos são truncados em 64 KB. O IP de origem dura mais que esses 30 dias: ele também é gravado na linha de uso que cobra a chamada, e essa linha é um registro financeiro que guardamos por mais tempo. A única coisa que não é armazenada é a requisição que o strict recusa antes de chegar ao modelo: ela aparece no seu histórico de uso sem o conteúdo. Fora isso não há exceção, e não existe opção para desligar. Ou seja: a amostra de malware que passa pela sua triagem fica gravada do nosso lado durante esse prazo — e o prompt também é encaminhado ao provedor que hospeda o modelo para ser respondido, e o que ele faz com isso é regido pelos termos dele. Se o seu material não pode existir em cópia fora do seu perímetro, essa é uma restrição real: leia a política de privacidade antes de plugar o pipeline, não depois.

Sobre escopo

Nada disso muda quem responde pelo resultado. A política de uso delimita o terreno: teste de sistemas que você tem autorização para testar, avaliação, detecção, defesa e pesquisa — de um lado; operacionalizar dano contra sistemas ou pessoas reais — do outro. A distinção prática é autorização e intenção, não vocabulário: "pesquisa de segurança" como rótulo não converte uma intrusão não autorizada em pesquisa.

Se o seu pipeline de avaliação está registrando recusas como resultados, pegue uma chave e troque duas linhas para ver quanto do seu baseline era instrumento em vez de dado.