MLflow's basic-auth plugin is a Flask before-request hook that resolves each request's protobuf class against BEFORE_REQUEST_HANDLERS, a plain Python dict holding 120 entries at tag v3.15.2. When the lookup misses, _find_validator returns None, _before_request skips the check, and the request proceeds for any authenticated caller, so an endpoint absent from the map is unguarded rather than denied. Only the /mlflow/traces/ prefix fails closed, and it does so because those endpoints already produced their own advisory, CVE-2026-8147 at CVSS 8.1. Ten advisories published between 2026-04-03 and 2026-08-17 describe a missing or misapplied authorization check on an MLflow endpoint: CVE-2026-69146 (6.5) was LogInputs missing from the map, and its fix in the auth plugin was four added lines, two of them imports; CVE-2026-69148 (7.1) was CreateModelVersion present in the map but checked against the registered model rather than the source run. CVE-2026-71211 (7.1, published 2026-08-05) is the open one: CreateGatewaySecret has no entry in the map, the vulnerable range covers 3.15.2, and no fixed version is recorded. The file MLflow ships still reads default_permission = READ with admin and password1234. Get to 3.15.2, then open your basic_auth.ini and change default_permission to NO_PERMISSIONS.
MLflow tracking server security has one load-bearing mechanism, and it is a dictionary. The basic-auth plugin decides whether to authorize a request by looking that request's protobuf class up in BEFORE_REQUEST_HANDLERS, a plain Python dict declared at line 2605 of mlflow/server/auth/__init__.py. At tag v3.15.2 it holds 120 entries across 16 commented route groups, from experiments and runs through gateway budget policies and review queues. Coverage is per endpoint by construction, so the question that decides your posture is not what the dict contains, it is what happens when the lookup misses.
It passes. _find_validator ends on return None, _before_request calls a validator only when it found one, and None is falsy, so a request to an endpoint that is not a key in the map proceeds for any authenticated caller. That is not a retrospective about bugs already fixed. At v3.15.2 the map holds GetGatewaySecretInfo, UpdateGatewaySecret and DeleteGatewaySecret, and it contains no entry for CreateGatewaySecret. CVE-2026-71211, published 2026-08-05 at CVSS 7.1, states it directly: the action has no entry in the permission-validator map, so it requires only basic authentication rather than any specific scope, and any authenticated user including a read-only account can create a secret pointing at an internal address and reach it through the proxy endpoint. Its vulnerable range runs from 3.13.0 through 3.15.2, with no first patched version recorded.
Everything below was read against the MLflow source at tag v3.15.2, the current release on PyPI since 2026-08-26. Start by finding out which version you are on.
mlflow --version # mlflow, version 3.15.2
What --app-name basic-auth actually turns on
The plugin is loaded with mlflow server --app-name basic-auth. The --app-name option is a click.Choice over the entry points registered under the mlflow.app group, swapping the WSGI app for one that wraps MLflow's handlers in HTTP basic authentication plus a per-resource grant table with five levels.
Those five names are the Permission dataclass instances in mlflow/server/auth/permissions.py and the keys of ALL_PERMISSIONS. The level you grant is EDIT; can_update is a dataclass field and validate_can_update_run a validator name, so neither appears in the permission API. The documentation lists five resource types the levels apply to: experiment, registered model, prompt, scorer and the AI Gateway resources. Grants are managed over REST at /api/3.0/mlflow/users/permissions/grant, /revoke and /get, each mirrored under /ajax-api/3.0/. The grant call takes exactly four parameters: username, resource_type, resource_id and permission; revoke and get take the first three. A role-management surface sits at the same API version, gated on validate_can_manage_roles, so this is a real authorization model in the shape we set out in role-based access control for AI applications, not a bolted-on password check.
The defaults are the problem. MLflow ships mlflow/server/auth/basic_auth.ini with default_permission = READ, database_uri = sqlite:///basic_auth.db, admin_username = admin and admin_password = password1234, and the same credential pair is printed on the documentation page. That default account has its own history: CVE-2026-2635, Critical at CVSS 9.8, published 2026-02-21 and first patched in 3.8.0rc0, is titled as a default-password authentication bypass.
default_permission is the subtler one, because it is not only a default for users nobody has granted anything to. _get_experiment_permission and its siblings fall back to get_permission(auth_config.default_permission) whenever no explicit grant exists, which makes it a floor under every resource. Leave it at the shipped READ and every account that can log in reads every experiment, including the ones created after the reviewer looked. Point MLFLOW_AUTH_CONFIG_PATH at your own copy and set the floor to the one level that grants nothing.
# /etc/mlflow/basic_auth.ini [mlflow] default_permission = NO_PERMISSIONS database_uri = postgresql://mlflowauth@db.internal/mlflow_auth admin_username = mlflow-admin admin_password = <injected from your secret store at start> authorization_function = mlflow.server.auth:authenticate_request_basic_auth grant_default_workspace_access = false
One operational note: MLFLOW_FLASK_SERVER_SECRET_KEY has to be set and identical across replicas, because the plugin uses it for CSRF protection on the signup page.
| Level | can_read | can_use | can_update | can_delete | can_manage |
|---|---|---|---|---|---|
| READ (the shipped default) | yes | no | no | no | no |
| USE | yes | yes | no | no | no |
| EDIT | yes | yes | yes | no | no |
| MANAGE | yes | yes | yes | yes | yes |
| NO_PERMISSIONS | no | no | no | no | no |
The lookup table, and what happens on a miss
Here is the authorization decision itself, copied from the file.
# mlflow/server/auth/__init__.py (v3.15.2), inside _before_request
# authorization
if validator := _find_validator(request):
if not validator():
return make_forbidden_response()
elif _is_proxy_artifact_path(request.path):
if validator := _get_proxy_artifact_validator(request.method, request.view_args):
if not validator():
return make_forbidden_response()The walrus operator is doing the damage. When _find_validator returns None the first branch is skipped, the elif only catches the three artifact proxy prefixes, and the function falls off the end returning None, which Flask reads as carry on with the request. Denial is an affirmative act here: make_forbidden_response() returns the body Permission denied with status 403, reached only when a validator existed and said no.
_find_validator resolves in four steps. Paths under /mlflow/logged-models are regex-matched against the map built from LOGGED_MODEL_BEFORE_REQUEST_HANDLERS (8 entries) and paths under /mlflow/webhooks against the one built from WEBHOOK_BEFORE_REQUEST_HANDLERS (6 entries, every one of them mapped to sender_is_admin), because those routes carry path parameters. Then an exact (path, method) lookup in the map built from BEFORE_REQUEST_HANDLERS. Then a regex pass over /mlflow/traces/. Then return None.
That last branch is the exception that proves the design. The /mlflow/traces/ prefix is the only one that fails closed: on a miss it returns lambda: False, with a comment saying unknown paths under this prefix are denied rather than skipped. It behaves that way because the trace endpoints already produced CVE-2026-8147, High at CVSS 8.1, published 2026-07-02 and first patched in 3.13.0rc0, whose description says the issue arises from _before_request not registering authorization validators for trace endpoints, so the requests proceed without validation. One prefix in the file learned the lesson. The mechanism did not change.
One unconditional bypass sits above the lookup: is_unprotected_route skips authentication for /static, /favicon.ico and /health. That list is small and deliberate, which is what the authorization map is not.
So coverage is a property of the release you run rather than of the policy you wrote. Every new endpoint starts unguarded and stays that way until somebody adds a line to a dict in a different file from the handler. That is the same argument about PostgreSQL roles in a different system: the layer you audit is not the boundary.
CVE-2026-69146: LogInputs was not in the map, and neither was LogOutputs
The purest example of the mechanism is a moderate-severity bug. CVE-2026-69146, CVSS 6.5, vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N, published 2026-08-17, affected everything below 3.15.0. The advisory's own comparison is the tell: the LogInputs proto handler was absent from BEFORE_REQUEST_HANDLERS, so the before-request hook skipped authorization entirely and the request succeeded, while standard write endpoints on the same run such as POST /api/2.0/mlflow/runs/log-metric correctly returned HTTP 403. Same run, same user, same server, two answers, decided by whether a class was a key in a dict.
Read the vector. Confidentiality impact is none and integrity impact is high: this let an authenticated user inject dataset records into another user's run through POST /api/2.0/mlflow/runs/log-inputs, corrupting lineage rather than leaking it. In a provenance record an auditor signs off on, that is the impact that matters anyway.
The fix is the whole story.
# mlflow/server/auth/__init__.py (v3.15.2), BEFORE_REQUEST_HANDLERS
# Routes for runs
CreateRun: validate_can_update_experiment,
GetRun: validate_can_read_run,
DeleteRun: validate_can_delete_run,
RestoreRun: validate_can_delete_run,
UpdateRun: validate_can_update_run,
LogMetric: validate_can_update_run,
LogBatch: validate_can_update_run,
LogInputs: validate_can_update_run, # added by #24291
LogModel: validate_can_update_run,
LogOutputs: validate_can_update_run, # added by #24291
SetTag: validate_can_update_run,PR #24291 was opened at 07:17 UTC on 2026-07-06 and merged at 09:26 UTC the same morning, 57 additions across two files. The change to the auth plugin is four added lines and no deletions: two imports and the two dictionary entries above. The advisory names only LogInputs; the merged patch added LogOutputs, which had the identical gap and no identifier of its own. The tests that shipped with it assert that a second user gets Permission denied and then succeeds once granted EDIT on the experiment.
Merging a well-written report in two hours says good things about the project. Two dict entries closing a CVSS 6.5 bypass says something else about the design: those four lines have to be written correctly for every endpoint anyone adds next quarter.
CVE-2026-69148: the right permission checked on the wrong subject
Presence in the table is necessary and not sufficient. CVE-2026-69148, High at CVSS 7.1 with C:H, published the same day and also fixed in 3.15.0, is the second shape of the defect: CreateModelVersion was in the map the whole time, pointed at the wrong validator.
# before PR #24293
CreateModelVersion: _validate_can_update_registered_model_or_prompt,
# after PR #24293, shipped in 3.15.0
CreateModelVersion: validate_can_create_model_version,Creating a model version is two operations wearing one endpoint: you write to a registered model, and you read from a source run or logged model named by run_id or model_id in the request body. The old mapping checked the caller against the thing written to and never against the thing read from, so a user with EDIT on their own registered model could point source at another team's run and create a version referencing it.
MLflow does validate source, which is what makes this instructive. _validate_source_run in mlflow/server/handlers.py is still shaped this way at v3.15.2:
# mlflow/server/handlers.py (v3.15.2)
def _validate_source_run(source: str, run_id: str) -> None:
if is_local_uri(source):
if run_id:
store = _get_tracking_store()
run = store.get_run(run_id) # no permission check on run_id
source = pathlib.Path(local_file_uri_to_path(source)).resolve()
if is_local_uri(run.info.artifact_uri):
run_artifact_dir = pathlib.Path(
local_file_uri_to_path(run.info.artifact_uri)
).resolve()
if run_artifact_dir in [source, *source.parents]:
returnIt confirms the source path sits inside the referenced run's artifact directory. That is a path-containment check, a data-integrity guarantee that a model version points at real artifacts, never an access control; _validate_source_model does the same against model.artifact_location. The fix went into the auth plugin instead: validate_can_create_model_version requires update on the registered model, then reads the request body and returns false unless the caller has read on the referenced run_id or model_id. Its own comment explains the exposure: without a read check a caller could point source at another user's run and read those artifacts through their own registered model.
Then the artifacts flow, because of one docstring. GET /get-artifact maps to validate_can_read_run_artifact, whose subject is the run id. GET /model-versions/get-artifact maps to validate_can_read_model_version_artifact, whose helper notes that model versions inherit permissions from their registered model. Two routes, two subjects, and a laundering path between them that neither route can see.
Ten advisories, one defect, and one still open
Ten advisories in the GitHub Advisory Database for the pip package mlflow, published between 2026-04-03 and 2026-08-17, describe a missing or misapplied authorization check on an endpoint. That is not a list of MLflow's 2026 CVEs; the database carries more in the same window covering arbitrary file reads, cross-site scripting and insecure temporary file permissions. It is the subset sharing one root cause, and reading them as ten patch tickets is how a team patches the same defect ten times.
CVE-2026-71211 is the row to act on, because an upgrade does not close it. The REST path is POST /api/3.0/mlflow/gateway/secrets/create, declared in mlflow/protos/service.proto since API major 3 minor 0. CreateGatewaySecret does appear elsewhere in the auth plugin: it is in AFTER_REQUEST_PATH_HANDLERS, mapped to set_can_manage_gateway_secret_permission, which grants the creator MANAGE on the secret after the request has already executed. The plugin knows the resource type exists, knows it has an owner and has an opinion about who should manage it. The one place it does not appear is the map that decides whether the call happens at all.
| Advisory | CVSS | What was not gated | Fixed in |
|---|---|---|---|
| CVE-2026-0545 | 9.1 critical | FastAPI job endpoints under /ajax-api/3.0/jobs/ had no authentication or authorization | range ends at 3.10.1; no fixed version recorded |
| CVE-2026-33866 | 4.3 medium | Authorization bypass on the AJAX endpoint mirror paths | 3.11.0 |
| CVE-2026-2652 | 8.6 high | Unauthenticated access to certain FastAPI routes | 3.11.0 |
| CVE-2026-2734 | 6.5 medium | Registered model versions enumerable without per-model permission checks | 3.10.0 |
| CVE-2026-2651 | 9.0 critical | Multipart upload endpoints unguarded when --serve-artifacts is on | 3.11.0 |
| CVE-2026-3198 | 6.5 medium | Gateway secrets, endpoints and model definitions enumerable by any authenticated user | 3.11.0 |
| CVE-2026-8147 | 8.1 high | Trace API endpoints had no authorization validators registered in _before_request | 3.13.0 |
| CVE-2026-69146 | 6.5 medium | LogInputs absent from BEFORE_REQUEST_HANDLERS | 3.15.0 |
| CVE-2026-69148 | 7.1 high | CreateModelVersion mapped to the registered-model validator, so the source run was never checked | 3.15.0 |
| CVE-2026-71211 | 7.1 high | CreateGatewaySecret has no entry in the permission-validator map | no fixed version; range covers 3.15.2 |
The artifact store is a second control plane
The permission table reaches only traffic that passes through MLflow, and two flags decide which artifact traffic does: --serve-artifacts/--no-serve-artifacts defaults to on, and --artifacts-destination defaults to ./mlartifacts, applying only when the experiment's artifact root is an http or mlflow-artifacts URI.
The split row is not hypothetical. The help text for --default-artifact-root says the flag does not impact already-created experiments with any previous configuration of a server instance. Flip a team from a direct S3 root to proxied serving and every older experiment keeps resolving artifacts client-side, so the permission grid describes access to some of your model weights and nothing about the rest.
Inside the proxied mode the coverage is narrow and explicit. _is_proxy_artifact_path recognises three prefixes under both /api/2.0 and /ajax-api/2.0: /mlflow-artifacts/artifacts, /mlflow-artifacts/mpu/ and /mlflow-artifacts/presigned/, and _get_proxy_artifact_validator maps GET to the read validator, PUT and POST to the update validator, DELETE to the delete validator. Anything outside those three prefixes gets no proxy fallback, which is the shape behind CVE-2026-2651, Critical at 9.0, where multipart upload endpoints were unguarded while --serve-artifacts was on. CreatePresignedDownloadUrl shows the design working: it maps to validate_can_read_run, with an inline comment that minting a presigned URL grants direct read access to a run's artifacts and so needs the same per-run READ as the proxied download paths. Somebody thought that one through, per endpoint.
Those seven rows are the honest version of the diagram a control tester gets shown. In a bank, an insurer or a pharma manufacturer, MLflow basic-auth is what somebody points at when the question is who can promote a model to production, and the evidence is a screenshot of the permission grid. The grid is real. What it cannot show is that enforcement is a dict lookup whose key set is not the endpoint list.
For a private-perimeter deployment, check the second control plane first. An on-prem MinIO or internal S3-compatible store that training jobs reach directly is governed by bucket policy, so if the experiments holding your production models were created with a direct artifact root, the MLflow grants in your audit evidence say nothing about who can read the weights. That is the split-control-plane problem we worked through for retrieval infrastructure, where the index is a second data copy with a different access model. List every experiment's artifact root before the audit, and for the direct ones write the bucket policy into the control narrative as the enforcement point.
| Mode | How it is configured | Who enforces artifact access |
|---|---|---|
| Proxied (the default) | --serve-artifacts on, --artifacts-destination points at the store, experiment artifact roots are mlflow-artifacts:// URIs | MLflow, through the experiment artifact proxy validators. One credential reaches the object store: the server's |
| Direct | --no-serve-artifacts, experiment artifact root is an s3:// or abfss:// URI the client resolves itself | The object store. MLflow grants describe intent; the bucket policy decides |
| Split | Experiments created before the switch keep their old artifact root, because --default-artifact-root only affects newly created experiments | Both, per experiment, and only an inventory tells you which |
| Surface | Gated by the MLflow permission table? | Where the control actually lives |
|---|---|---|
| Experiment and run REST calls | Only if the proto request class is a key in BEFORE_REQUEST_HANDLERS | 120 dict entries in mlflow/server/auth/init.py |
| Proxied artifacts under /mlflow-artifacts/artifacts, /mpu/, /presigned/ | Yes, through the _is_proxy_artifact_path fallback | Experiment-level artifact proxy validators |
| Artifacts read directly from s3:// or abfss:// by the client | No | Bucket or container IAM policy; MLflow is not in the request path |
| GET /get-artifact | Yes | Permission resolved from the run id |
| GET /model-versions/get-artifact | Yes, but the subject is the registered model | Model versions inherit permission from their registered model, which is what CVE-2026-69148 exploited |
| Webhook routes | Admin only when the plugin is loaded; unauthenticated when it is not | WEBHOOK_BEFORE_REQUEST_HANDLERS, all six entries mapped to sender_is_admin |
| Host header, CORS, clickjacking | No | Security middleware added in 3.5.0, configured by --allowed-hosts and --cors-allowed-origins |
The registry is a delivery channel, and the artifact names its own loader
The argument about MLflow's pickle guard is about the flavor system, not about scanning. MLFLOW_ALLOW_PICKLE_DESERIALIZATION is a boolean environment variable at mlflow/environment_variables.py line 1613 whose default is True, so unsafe deserialization is permitted unless an operator turns it off. It exists because of the unsafe-deserialization series around CVE-2024-37052, High at 8.8 in 2024.
The guard is not enforced where models get loaded. mlflow.pyfunc.load_model dispatches with importlib.import_module(conf[MAIN])._load_pyfunc(data_path), where MAIN is the string loader_module, read out of the MLmodel file inside the artifact. The artifact names the module that will be imported into your process, and there is no pickle gate at that dispatch point. Each flavor writes the check out by hand instead: at v3.15.2, nine files under mlflow/ reference the variable besides its definition, across the dspy, pmdarima, statsmodels, langchain, pyfunc, pytorch, sklearn and tensorflow modules plus model evaluation artifacts. A control implemented by repetition fails the way repetition fails, one file at a time.
GHSA-gqvg-gmmx-x4hm, published 2026-09-01 at CVSS 8.8 with no CVE assigned, is that failure: the mlflow.statsmodels flavor had no guard at all, so a crafted model artifact executed on load even where the variable was set to false. Affected range >= 2.1.0, < 3.15.0; the fix, merged 2026-07-27, is 26 added lines across the flavor module and its test file, following an earlier fix in the same series merged 2026-03-05. Read the population that bug could reach: the default is True, so the only deployments where setting it false meant anything were the disciplined ones, and those were exactly the ones the bypass affected.
A registry is a hard place to catch this, and not because scanning is weak. We covered vetting model files before they enter your estate separately, and that is the right control at the boundary. The registry is downstream of it: the artifact arrives already trusted, then loads weeks later in a batch scoring job whose owner never saw it arrive and is reading a stage label rather than a hash. Intake is where a file gets judged; the registry is where it gets executed.
The version floor, the proxy boundary, and what to change this week
Get to 3.15.2. 3.15.0 landed on PyPI on 2026-07-31 and is the first patched version for CVE-2026-64849, CVE-2026-69148, CVE-2026-69146 and the statsmodels bypass; 3.15.1 followed on 2026-08-03 and 3.15.2 on 2026-08-26. There is no 3.13.1 and no 3.14.1, so there is no backport line to hope for. We ran the same version floor exercise for a vLLM inference server, with one difference: here the upgrade demonstrably does not finish the job, because CVE-2026-71211 has no fixed version.
CVE-2026-64849 is worth understanding rather than just patching. Critical at CVSS 9.3, published 2026-08-17, it is a full-read SSRF in webhook delivery, and its description records the structural fact underneath this post: webhook endpoints are unauthenticated on a default server, because the only webhook authorization lives in the optional auth plugin. The original guard, _validate_webhook_url in mlflow/utils/validation.py, shipped in 3.10.0 and does its job on the obvious input:
POST /api/2.0/mlflow/webhooks
Content-Type: application/json
{"name":"neg","url":"http://127.0.0.1:6379/","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}
400 {"message":"Invalid webhook URL scheme: 'http'. Allowed schemes are: https."}A URL check that runs before the request is sent cannot survive a name that resolves differently later or a response that redirects elsewhere, so the fix moved to connection time rather than adding a redirect flag: the merged change validates the peer IP of the connected socket immediately after connect() returns, before any TLS handshake or HTTP data is exchanged.
Then set the perimeter. MLflow's --host defaults to 127.0.0.1, and its help text says this is not a security setting, it only controls network binding, and that restricting clients is --allowed-hosts. But --allowed-hosts defaults to localhost variants plus the private IP patterns 192.168.*, 10.* and 172.16.* through 172.31.*. On a corporate network that admits every RFC 1918 address, so it stops browser-based DNS rebinding and nothing else.
export MLFLOW_FLASK_SERVER_SECRET_KEY="$(openssl rand -hex 32)" # same value on every replica export MLFLOW_AUTH_CONFIG_PATH=/etc/mlflow/basic_auth.ini mlflow server \ --app-name basic-auth \ --backend-store-uri postgresql://mlflow@db.internal/mlflow \ --artifacts-destination s3://ml-artifacts-prod \ --host 127.0.0.1 \ --port 5000 \ --allowed-hosts mlflow.internal.example.com \ --cors-allowed-origins https://mlflow.internal.example.com
One trap: _validate_server_args raises a click.UsageError if --allowed-hosts, --cors-allowed-origins or --disable-security-middleware are passed on the command line together with --gunicorn-opts or --waitress-opts. Those three security options are supported only on the default uvicorn server. The environment-variable forms are exempt, so tune workers and set MLFLOW_SERVER_ALLOWED_HOSTS instead.
Binding to loopback presumes something in front, and that something is the only place a per-caller identity from your own IdP exists: MLflow's extension point (authorization_function in the ini) replaces the credential check and leaves the coverage question where it was. The annotated configuration for that boundary is in our post on making the reverse proxy the identity boundary; what belongs here is the block list you put on it.
Those six rules are worth more than the upgrade, because they hold whatever version you run.
This week: upgrade to 3.15.2 and confirm it with mlflow --version on the running host rather than in the deploy manifest, change default_permission to NO_PERMISSIONS in your own ini file, then resolve one real user's effective permission the way the server does, because a grant you believe in is not a grant the code found:
# what permission does this user actually resolve to on this experiment?
curl -sS -u "$MLFLOW_TRACKING_USERNAME:$MLFLOW_TRACKING_PASSWORD" \
"$MLFLOW_TRACKING_URI/api/3.0/mlflow/users/permissions/get?username=bob&resource_type=experiment&resource_id=2"
# grant, once you know what it should be
curl -sS -u "$MLFLOW_TRACKING_USERNAME:$MLFLOW_TRACKING_PASSWORD" -X POST \
-H 'Content-Type: application/json' \
-d '{"username":"bob","resource_type":"experiment","resource_id":"2","permission":"EDIT"}' \
"$MLFLOW_TRACKING_URI/api/3.0/mlflow/users/permissions/grant"That call answers what the grant table says. It cannot tell you whether the endpoint your user is about to hit has an entry in the map, and the durable answer to that is an identity boundary and a block list outside the application. The rest of our work on that boundary sits in our AI security coverage.
| Path | Method | Why |
|---|---|---|
| /api/2.0/mlflow/webhooks and /ajax-api/2.0/mlflow/webhooks | all | Unauthenticated on a server without the plugin, admin only with it. This is the CVE-2026-64849 surface |
/api/2.0/mlflow/webhooks/<id>/test | POST | Returns the upstream status and body to the caller, which is what turned an SSRF into a full-read SSRF |
| /api/3.0/mlflow/gateway/secrets/create | POST | No entry in BEFORE_REQUEST_HANDLERS at 3.15.2. CVE-2026-71211 has no fixed version |
| /ajax-api/3.0/jobs/ | all | The prefix behind CVE-2026-0545. Block it unless server-side job execution is a feature you use |
| /signup and /api/2.0/mlflow/users/create | GET, POST | Self-service account creation. In a regulated estate accounts come from the IdP, not from the app |
| /metrics | GET | Only present when --expose-prometheus is set, and it has no validator entry, so any authenticated caller reads workload telemetry. On a server without the plugin, anyone does |
FAQ
Quick answers to the questions this post tends to raise.


