{"id":"GHSA-qrj2-j723-m6w8","kind":"vulnerability","aliases":["GHSA-qrj2-j723-m6w8"],"related":[],"title":"vLLM Speculators Config verifier.name_or_path Element Directory Traversal Information Disclosure Vulnerability","description":"### Summary\n\n`maybe_override_with_speculators` runs unconditionally for every non-cloud-storage model (`vllm/engine/arg_utils.py:2218`). If the served repository's `config.json` carries a `speculators_config` key, vLLM takes `speculators_config[\"verifier\"][\"name_or_path\"]` straight out of that artifact and assigns it to **both** the model and the tokenizer:\n\n```python\n# vllm/transformers_utils/config.py:808-810\nverifier_model = speculators_config[\"verifier\"][\"name_or_path\"]\nmodel = tokenizer = verifier_model\n```\n\nThere is no containment check of any kind — no `basename`, no `resolve()` + `commonpath`, no `relative_to`/`parents` test, no `isabs` rejection. An absolute path needs no traversal syntax at all. The only validation anywhere on this path checks that four keys *exist* (`vllm/transformers_utils/configs/speculators/base.py:77-94`); nothing inspects what `name_or_path` points at.\n\nServing a crafted repository therefore redirects the whole served model identity — weights, config and tokenizer — to a directory the artifact names. Confirmed end-to-end against a live `vllm serve`: the unauthenticated `POST /detokenize` returns the victim directory's tokenizer vocabulary verbatim, and the victim checkpoint is what the engine loads as the primary model.\n\nLine numbers above are at `main` @ `e0317b28ce3ba4c9bd2ff69d885e3f34688a6d71`, where no containment fix has landed on `vllm/transformers_utils/config.py`. No opt-in is required: `trust_remote_code` stays `False` throughout (the engine prints it at startup).\n\n### Details\n\n### The call site\n\nThe override is not gated on `--speculative-config`; it runs for every model that is not cloud storage, before `ModelConfig` is constructed:\n\n```python\n# vllm/engine/arg_utils.py:2218-2228\nif not is_cloud_storage(self.model):\n    (self.model, self.tokenizer, self.speculative_config) = (\n        maybe_override_with_speculators(\n            model=self.model,\n            tokenizer=self.tokenizer,\n            revision=self.revision,\n            trust_remote_code=self.trust_remote_code,\n            ...\n```\n\n### The redirect\n\n```python\n# vllm/transformers_utils/config.py:786-810\nconfig_dict, _ = PreTrainedConfig.get_config_dict(model, ..., **without_trust_remote_code(kwargs))\nspeculators_config = config_dict.get(\"speculators_config\")\nif speculators_config is None:\n    return model, tokenizer, vllm_speculative_config\n...\n# Override model and tokenizer with the verifier model from config\nverifier_model = speculators_config[\"verifier\"][\"name_or_path\"]\nmodel = tokenizer = verifier_model          # :810 — no containment, no isabs check\n```\n\nNo `basename`, no `resolve()` + `commonpath`, no `relative_to`/`parents` test, no `isabs` rejection. An absolute path needs no traversal syntax. Note `without_trust_remote_code(kwargs)` at `:786`: this path deliberately reads the config *without* remote code, which is why the redirect needs no opt-in.\n\n### The validation is key-existence only\n\n```python\n# vllm/transformers_utils/configs/speculators/base.py:77-94\nspec_config = config_dict[\"speculators_config\"]\nmethods = spec_config[\"proposal_methods\"]\nfirst_method = methods[0]\n_ = first_method[\"speculative_tokens\"]\n_ = spec_config[\"verifier\"][\"name_or_path\"]      # presence only\n_ = config_dict[\"speculators_model_type\"]\nexcept (KeyError, IndexError, TypeError) as e:\n    raise ValueError(\"Invalid speculators config structure\") from e\n\nif \"transformer_layer_config\" not in config_dict:\n    raise ValueError(\"Must provide transformer_layer_config\")\n```\n\nNothing inspects what `name_or_path` points at. All of these keys are attacker-written; `speculators_model_type` must be one of the in-tree `@register_speculator` names (`eagle3`, `peagle`, `dflash`, `dflash2`, `dspark` — `vllm/transformers_utils/configs/speculators/algos.py`), which the attacker simply types.\n\n### Impact\n\nAn operator who serves an untrusted repository — a public Hub model, a vendor checkpoint, or a directory delivered by `git clone` or a shared mount — hands its publisher two things.\n\n**The victim directory's tokenizer vocabulary, verbatim, to an unauthenticated client.** `/detokenize` returns it as text, and sweeping token ids walks the whole vocabulary. No inversion, no numerical attack, no credentials. For a checkpoint whose tokenizer is a public base this is of little value; for a domain-specific or in-house vocabulary it is the model's IP, and it also discloses the domain, the languages, the special tokens and internal naming.\n\n**Execution of the victim's checkpoint.** The engine loads it as the primary model, so the attacker obtains full black-box query access to a private model through the ordinary completions API — which is the practical substance of model theft, since behavioural cloning needs no weight recovery. The operator, meanwhile, is silently serving someone else's weights under their own service.\n\nShared and multi-tenant model hosts are the realistic setting, since that is where the reachable directories belong to someone other than the repository's publisher.\n\nA fix for this was introduced in https://github.com/vllm-project/vllm/pull/61587.","summary":"vLLM trusts a path in a model repository's config.json (speculators_config) and loads the directory it names as the served model and tokenizer, with no containment check. An attacker who publishes a crafted model can make an operator's server read a directory of someone else's model and expose its tokenizer vocabulary and weights through the API.","severity":"medium","cvss":5.4,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"vllm","fixed":["0.33.0"],"affected":"< 0.33.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["ai-tooling","cwe-200","cwe-22","model-serving","python","self-hosted","vllm"],"not_affected_if":["only-trusted-local-models","vllm-at-or-above-0.33.0"],"how_to_check":"- Check the installed vLLM version with `pip show vllm`; versions below `0.33.0` are affected.\n- Serve only models you trust: look for a `speculators_config` key in any model's `config.json` that you did not write.\n- Check whether vLLM is serving models from untrusted Hub repositories, git clones or shared mounts.","fix":"- `vllm` (pypi): move to `0.33.0` or later.","status":"open","published_at":"2026-11-02T19:49:32+00:00","first_seen_at":"2026-11-09T22:53:23+00:00","triaged_at":"2026-11-09T22:56:23+00:00","updated_at":"2026-11-09T22:56:23+00:00","category":"ai-tooling","confidence":0.85,"triage":{"at":"2026-11-09T22:56:23Z","model":"claude-haiku-5-5","output":{"fix":"- `vllm` (pypi): move to `0.33.0` or later.","reach":"internet","title":"vLLM: crafted model repository redirects served model to a local directory","impact":"medium","summary":"vLLM trusts a path in a model repository's config.json (speculators_config) and loads the directory it names as the served model and tokenizer, with no containment check. An attacker who publishes a crafted model can make an operator's server read a directory of someone else's model and expose its tokenizer vocabulary and weights through the API.","audience":"businesses","category":"ai-tooling","nicknames":[],"who_cares":["vllm","python","self-hosted","model-serving"],"confidence":0.85,"fix_status":"available","needs_auth":"no","worst_case":"Attacker reads a victim model's tokenizer and runs its weights through the API","analyst_note":"","how_to_check":"- Check the installed vLLM version with `pip show vllm`; versions below `0.33.0` are affected.\n- Serve only models you trust: look for a `speculators_config` key in any model's `config.json` that you did not write.\n- Check whether vLLM is serving models from untrusted Hub repositories, git clones or shared mounts.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["vllm-at-or-above-0.33.0","only-trusted-local-models"],"who_is_affected":"Any team that runs vLLM and serves models it did not fully write itself, such as public Hub checkpoints, vendor checkpoints, git-cloned directories or shared model mounts on multi-tenant hosts. Deployments that only serve models their own team built, from a closed source, are less exposed. The flaw is reached through the normal model loading path with no extra option needed.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"no","severity_if_unknown":"unknown"},"cost_usd":0.000962,"input_tokens":6409,"output_tokens":642,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-200':28B '-22':30B '-2228':336C '-810':151C,368C '-94':208C,481C '/detokenize':251C '0':496C '2218':113C,335C '77':207C,480C '786':367C,451C '808':150C '810':418C 'absolut':186C,438C 'ai':25B 'ai-tool':24B 'anywher':197C 'api':97B 'artifact':137C,236C 'assign':139C 'attack':68B,557C,581C 'attacker-written':556C 'basenam':173C,425C 'call':308C 'carri':120C 'check':66B,168C,201C,423C 'checkpoint':263C,598C 'clone':605C 'cloud':109C,327C,340C 'code':296C,359C,362C,378C,448C,460C 'commonpath':176C,428C 'config':3A,50B,123C,128C,155C,229C,318C,346C,369C,372C,381C,384C,387C,395C,405C,409C,457C,483C,484C,487C,490C,502C,509C,524C,531C,534C,542C 'config.json':48B,119C 'config_dict.get':382C 'confirm':238C 'construct':332C 'contain':65B,167C,282C,420C 'craft':72B,219C 'cwe':27B,29B 'deliber':454C 'deliv':602C 'detail':306C 'dflash':575C 'dflash2':576C 'dict':370C,373C,485C,510C,535C 'directori':8A,54B,82B,234C,255C,601C 'disclosur':11A 'dspark':577C 'e':519C,527C 'e0317b28ce3ba4c9bd2ff69d885e3f34688a6d71':279C 'eagle3':573C 'element':7A 'els':85B 'end':240C,242C 'end-to-end':239C 'engin':267C,301C 'everi':106C,322C 'except':514C 'exist':205C,476C 'expos':89B 'fals':298C 'first':493C,497C 'fix':283C 'four':203C 'gate':314C 'ghsa':14A,19A 'ghsa-qrj2-j723-m6w8':13A,18A 'git':604C 'han':610C 'host':37B 'hub':594C 'ident':227C 'impact':584C 'in-tre':567C 'indexerror':516C 'inform':10A 'inspect':210C,544C 'invalid':522C 'isab':183C,422C,435C 'j723':16A,21A 'key':124C,204C,475C,554C 'key-exist':474C 'keyerror':515C 'kind':171C 'kwarg':379C,449C 'land':285C 'layer':530C,541C 'line':273C 'live':245C 'load':52B,268C 'm6w8':17A,22A 'main':278C 'make':75B 'mayb':99C,347C 'method':488C,492C,494C,495C,498C 'model':32B,45B,60B,73B,87B,111C,144C,153C,160C,163C,226C,272C,323C,351C,374C,391C,397C,403C,407C,414C,417C,512C,560C,595C 'model-serv':31B 'modelconfig':330C 'mount':609C 'must':538C,562C 'name':56B,130C,157C,212C,237C,411C,504C,546C,572C 'need':188C,440C,466C 'non':108C 'non-cloud-storag':107C 'none':389C 'note':444C 'noth':209C,543C 'number':274C 'one':564C 'oper':77B,586C 'opt':290C,469C 'opt-in':289C,468C 'overrid':100C,311C,348C,396C 'parent':180C,432C 'path':6A,42B,132C,159C,187C,200C,214C,413C,439C,453C,506C,548C 'peagl':574C 'point':215C,549C 'post':250C 'presenc':507C 'pretrainedconfig.get':371C 'primari':271C 'print':302C 'propos':491C 'provid':539C 'public':593C 'publish':70B 'python':34B,148C,333C,365C,478C 'qrj2':15A,20A 'rais':520C,536C 'read':80B,455C 'redirect':222C,364C,465C 'regist':570C 'reject':184C,436C 'relat':178C,430C 'remot':295C,358C,361C,377C,447C,459C 'repositori':46B,117C,220C,591C 'requir':293C 'resolv':175C,427C 'return':252C,390C 'revis':355C 'run':103C,320C 'self':36B 'self-host':35B 'self.model':342C,343C,352C 'self.revision':356C 'self.speculative':345C 'self.tokenizer':344C,354C 'self.trust':360C 'serv':33B,59B,116C,217C,225C,247C,588C 'server':79B 'share':608C 'simpli':582C 'site':309C 'someon':84B 'spec':482C,489C,501C 'specul':2A,49B,102C,122C,127C,154C,317C,350C,380C,383C,386C,394C,408C,486C,499C,511C,523C,559C,571C 'speculative-config':316C 'startup':305C 'stay':297C 'storag':110C,328C,341C 'straight':133C 'structur':525C 'summari':98C 'syntax':191C,443C 'take':126C 'test':181C,433C 'therefor':221C 'throughout':299C 'token':62B,91B,147C,161C,231C,257C,353C,392C,399C,415C,500C 'tool':26B 'transform':529C,540C 'travers':9A,190C,442C 'tree':569C 'trust':40B,294C,357C,376C,446C 'type':513C,561C,583C 'typeerror':517C 'unauthent':249C 'uncondit':104C 'untrust':590C 'valid':196C,472C 'valueerror':521C,537C 'vendor':597C 'verbatim':259C 'verifi':129C,152C,156C,162C,402C,406C,410C,416C,503C 'verifier.name':4A 'victim':254C,262C 'vllm':1A,23A,38B,39B,125C,246C,393C 'vllm/engine/arg_utils.py':112C,334C 'vllm/transformers_utils/config.py':149C,287C,366C 'vllm/transformers_utils/configs/speculators/algos.py':578C 'vllm/transformers_utils/configs/speculators/base.py':206C,479C 'vocabulari':92B,258C 'vulner':12A 'weight':94B,228C 'whole':224C 'without':375C,445C,458C 'written':558C","nicknames":[],"mitigations":[],"who_affected":"Any team that runs vLLM and serves models it did not fully write itself, such as public Hub checkpoints, vendor checkpoints, git-cloned directories or shared model mounts on multi-tenant hosts. Deployments that only serve models their own team built, from a closed source, are less exposed. The flaw is reached through the normal model loading path with no extra option needed.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["vllm"],"ptokens":["vllm"],"labels":["aitooling","cwe200","cwe22","medium","modelserving","pypi","python","selfhosted","vllm"],"campaign_type":null,"search_a":"'config':3A 'directori':8A 'disclosur':11A 'element':7A 'ghsa':14A,19A 'ghsa-qrj2-j723-m6w8':13A,18A 'inform':10A 'j723':16A,21A 'm6w8':17A,22A 'path':6A 'qrj2':15A,20A 'specul':2A 'travers':9A 'verifier.name':4A 'vllm':1A,23A 'vulner':12A","search_b":"'-200':28B '-22':30B 'ai':25B 'ai-tool':24B 'api':97B 'attack':68B 'check':66B 'config':50B 'config.json':48B 'contain':65B 'craft':72B 'cwe':27B,29B 'directori':54B,82B 'els':85B 'expos':89B 'host':37B 'load':52B 'make':75B 'model':32B,45B,60B,73B,87B 'model-serv':31B 'name':56B 'oper':77B 'path':42B 'publish':70B 'python':34B 'read':80B 'repositori':46B 'self':36B 'self-host':35B 'serv':33B,59B 'server':79B 'someon':84B 'specul':49B 'token':62B,91B 'tool':26B 'trust':40B 'vllm':38B,39B 'vocabulari':92B 'weight':94B","search_c":"'-2228':336C '-810':151C,368C '-94':208C,481C '/detokenize':251C '0':496C '2218':113C,335C '77':207C,480C '786':367C,451C '808':150C '810':418C 'absolut':186C,438C 'anywher':197C 'artifact':137C,236C 'assign':139C 'attack':557C,581C 'attacker-written':556C 'basenam':173C,425C 'call':308C 'carri':120C 'check':168C,201C,423C 'checkpoint':263C,598C 'clone':605C 'cloud':109C,327C,340C 'code':296C,359C,362C,378C,448C,460C 'commonpath':176C,428C 'config':123C,128C,155C,229C,318C,346C,369C,372C,381C,384C,387C,395C,405C,409C,457C,483C,484C,487C,490C,502C,509C,524C,531C,534C,542C 'config.json':119C 'config_dict.get':382C 'confirm':238C 'construct':332C 'contain':167C,282C,420C 'craft':219C 'deliber':454C 'deliv':602C 'detail':306C 'dflash':575C 'dflash2':576C 'dict':370C,373C,485C,510C,535C 'directori':234C,255C,601C 'dspark':577C 'e':519C,527C 'e0317b28ce3ba4c9bd2ff69d885e3f34688a6d71':279C 'eagle3':573C 'end':240C,242C 'end-to-end':239C 'engin':267C,301C 'everi':106C,322C 'except':514C 'exist':205C,476C 'fals':298C 'first':493C,497C 'fix':283C 'four':203C 'gate':314C 'git':604C 'han':610C 'hub':594C 'ident':227C 'impact':584C 'in-tre':567C 'indexerror':516C 'inspect':210C,544C 'invalid':522C 'isab':183C,422C,435C 'key':124C,204C,475C,554C 'key-exist':474C 'keyerror':515C 'kind':171C 'kwarg':379C,449C 'land':285C 'layer':530C,541C 'line':273C 'live':245C 'load':268C 'main':278C 'mayb':99C,347C 'method':488C,492C,494C,495C,498C 'model':111C,144C,153C,160C,163C,226C,272C,323C,351C,374C,391C,397C,403C,407C,414C,417C,512C,560C,595C 'modelconfig':330C 'mount':609C 'must':538C,562C 'name':130C,157C,212C,237C,411C,504C,546C,572C 'need':188C,440C,466C 'non':108C 'non-cloud-storag':107C 'none':389C 'note':444C 'noth':209C,543C 'number':274C 'one':564C 'oper':586C 'opt':290C,469C 'opt-in':289C,468C 'overrid':100C,311C,348C,396C 'parent':180C,432C 'path':132C,159C,187C,200C,214C,413C,439C,453C,506C,548C 'peagl':574C 'point':215C,549C 'post':250C 'presenc':507C 'pretrainedconfig.get':371C 'primari':271C 'print':302C 'propos':491C 'provid':539C 'public':593C 'python':148C,333C,365C,478C 'rais':520C,536C 'read':455C 'redirect':222C,364C,465C 'regist':570C 'reject':184C,436C 'relat':178C,430C 'remot':295C,358C,361C,377C,447C,459C 'repositori':117C,220C,591C 'requir':293C 'resolv':175C,427C 'return':252C,390C 'revis':355C 'run':103C,320C 'self.model':342C,343C,352C 'self.revision':356C 'self.speculative':345C 'self.tokenizer':344C,354C 'self.trust':360C 'serv':116C,217C,225C,247C,588C 'share':608C 'simpli':582C 'site':309C 'spec':482C,489C,501C 'specul':102C,122C,127C,154C,317C,350C,380C,383C,386C,394C,408C,486C,499C,511C,523C,559C,571C 'speculative-config':316C 'startup':305C 'stay':297C 'storag':110C,328C,341C 'straight':133C 'structur':525C 'summari':98C 'syntax':191C,443C 'take':126C 'test':181C,433C 'therefor':221C 'throughout':299C 'token':147C,161C,231C,257C,353C,392C,399C,415C,500C 'transform':529C,540C 'travers':190C,442C 'tree':569C 'trust':294C,357C,376C,446C 'type':513C,561C,583C 'typeerror':517C 'unauthent':249C 'uncondit':104C 'untrust':590C 'valid':196C,472C 'valueerror':521C,537C 'vendor':597C 'verbatim':259C 'verifi':129C,152C,156C,162C,402C,406C,410C,416C,503C 'victim':254C,262C 'vllm':125C,246C,393C 'vllm/engine/arg_utils.py':112C,334C 'vllm/transformers_utils/config.py':149C,287C,366C 'vllm/transformers_utils/configs/speculators/algos.py':578C 'vllm/transformers_utils/configs/speculators/base.py':206C,479C 'vocabulari':258C 'weight':228C 'whole':224C 'without':375C,445C,458C 'written':558C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-02T19:49:32+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-qrj2-j723-m6w8","url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-qrj2-j723-m6w8","severity":"medium","published_at":"2026-11-02T19:49:32+00:00","modified_at":"2026-11-02T19:49:32+00:00","first_seen_at":"2026-11-09T22:53:23+00:00","fetched_at":"2026-11-09T22:53:23+00:00","note":null}],"documents":[{"id":110580,"url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-qrj2-j723-m6w8","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}