Root cause
SSRF & network boundaries introduced
Implement webhook dispatch logic (#16721)
How AI contributed: Direct introductionPublished May 11, 2026 in
mlflow/mlflowAll 7 findings in this repository
Server-Side Request Forgery (SSRF) in mlflow/mlflow. Sink: mlflow/environment_variables.py.
Root cause
Implement webhook dispatch logic (#16721)
Fix
Validate webhook url to fix SSRF vulnerability (#20747)
Fix by TomuHirata · no AI marker found
Case facts
@@ -0,0 +1,37 @@+import logging++import requests++from mlflow.entities.webhook import WebhookEvent+from mlflow.store.model_registry.abstract_store import AbstractStore+from mlflow.webhooks.types import WebhookPayload++_logger = logging.getLogger(__name__)+++def _dispatch_webhook_impl(+ *,+ event: WebhookEvent,+ payload: WebhookPayload,+ store: AbstractStore,+) -> None:+ # TODO: Make this non-blocking+ for webhook in store.list_webhooks():+ if event in webhook.events:+ # TODO: Implement HMAC signature verification+ requests.post(webhook.url, json=payload)+++def dispatch_webhook(+ *,+ event: WebhookEvent,+ payload: WebhookPayload,+ store: AbstractStore,+) -> None:+ try:+ _dispatch_webhook_impl(event=event, payload=payload, store=store)+ except Exception as e:+ _logger.error(+ f"Failed to dispatch webhook for event {event}: {e}",+ exc_info=True,+ )@@ -32,6 +32,7 @@ ) from mlflow.store.model_registry.abstract_store import AbstractStore from mlflow.store.model_registry.file_store import FileStore+from mlflow.utils.validation import _validate_webhook_url from mlflow.webhooks.constants import ( WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_SIGNATURE_HEADER,@@ -148,6 +149,8 @@ def _send_webhook_request( Returns: requests.Response object from the webhook request """+ _validate_webhook_url(webhook.url)+ # Create webhook payload with metadata webhook_payload = { "entity": event.entity.value,@@ -2,18 +2,21 @@ Utilities for validating user inputs such as metric names and parameter names. """ +import ipaddress import json import logging import numbers import posixpath import re+import socket import urllib.parse from typing import Any from mlflow.entities import Dataset, DatasetInput, InputTag, Param, RunTag from mlflow.entities.model_registry.prompt_version import PROMPT_TEXT_TAG_KEY from mlflow.entities.webhook import WebhookEvent from mlflow.environment_variables import (+ _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS, _MLFLOW_WEBHOOK_ALLOWED_SCHEMES, MLFLOW_ARTIFACT_LOCATION_MAX_LENGTH, MLFLOW_TRUNCATE_LONG_VALUES,@@ -737,6 +740,33 @@ def _validate_webhook_url(url: str) -> None: f"Allowed schemes are: {', '.join(schemes)}." ) + hostname = parsed_url.hostname+ if not hostname:+ raise MlflowException.invalid_parameter_value(+ f"Webhook URL must include a hostname: {url!r}"+ )++ if not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():+ try:+ addr_infos = socket.getaddrinfo(hostname, None)+ except socket.gaierror as e:+ raise MlflowException.invalid_parameter_value(+ f"Cannot resolve webhook URL hostname {hostname!r}: {e}"+ ) from e++ for addr_info in addr_infos:+ try:+ ip = ipaddress.ip_address(addr_info[4][0])+ except ValueError as e:+ raise MlflowException.invalid_parameter_value(+ f"Webhook URL hostname {hostname!r} resolved to an invalid IP address: {e}"+ ) from e+ if not ip.is_global:+ raise MlflowException.invalid_parameter_value(+ f"Webhook URL must not resolve to a non-public IP address. "+ f"{hostname!r} resolves to {ip}."+ )+ def _validate_webhook_events(events: list[WebhookEvent]) -> None: if (@@ -1162,6 +1162,14 @@ def _split_strip(s: str) -> list[str]: #: (default: ``60``) MLFLOW_WEBHOOK_CACHE_TTL = _EnvironmentVariable("MLFLOW_WEBHOOK_CACHE_TTL", int, 60) +#: Whether to allow webhook URLs that resolve to private/internal IP addresses.+#: When set to ``true``, SSRF protection is disabled and webhooks can target+#: localhost, private IPs, and cloud metadata endpoints.+#: This should only be enabled for local development. (default: ``false``)+_MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS = _BooleanEnvironmentVariable(+ "MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS", False+)+ #: Whether to disable telemetry collection in MLflow. If set to True, no telemetry #: data will be collected. (default: ``False``)AI-assisted change 8c02e45d83c25dfc8e548b4b84de598d768bc6576641464c2a2c84609d1dca36 · Fix 3bbb2d32f0a14a4cdecff0148d035cbe7ea950bb539133fd6b2db42f84d9275a