VYPR
High severity8.1NVD Advisory· Published Jan 12, 2026· Updated Apr 14, 2026

CVE-2025-14279

CVE-2025-14279

Description

MLFlow versions up to and including 3.4.0 are vulnerable to DNS rebinding attacks due to a lack of Origin header validation in the MLFlow REST server. This vulnerability allows malicious websites to bypass Same-Origin Policy protections and execute unauthorized calls against REST endpoints. An attacker can query, update, and delete experiments via the affected endpoints, leading to potential data exfiltration, destruction, or manipulation. The issue is resolved in version 3.5.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mlflowPyPI
< 3.5.03.5.0

Affected products

1

Patches

1
b0ffd289e9b0

Add a security middleware layer to MLflow Tracking Server (#17910)

https://github.com/mlflow/mlflowBen WilsonOct 6, 2025via ghsa
25 files changed · +4163 16
  • docs/docs/classic-ml/getting-started/logging-first-model/step1-tracking-server.mdx+7 0 modified
    @@ -45,6 +45,13 @@ Remember the host and port name that your MLflow tracking server is assigned. Yo
     this information in the next section of this tutorial!
     :::
     
    +:::info Security Note
    +Starting with MLflow 3.5.0, the server includes built-in security features using industry-standard middleware libraries
    +to protect against DNS rebinding and CORS attacks. By default, it only accepts connections from localhost.
    +If you need to expose your server to other machines, see the [MLflow Server Security Guide](/ml/tracking/server/security)
    +for proper configuration.
    +:::
    +
     Congratulations! Your MLflow environment is now set up and ready to go. As you progress, you'll
     explore the myriad of functionalities MLflow has to offer, streamlining and enhancing your machine learning workflows.
     
    
  • docs/docs/classic-ml/tracking/server/index.mdx+32 8 modified
    @@ -43,9 +43,10 @@ all network interfaces (or a specific interface address). This is typically
     required configuration when running the server **in a Kubernetes pod or a
     Docker container**.
     
    -Note that doing this for a server running on a public network is not recommended
    -for security reasons. You should consider using a reverse proxy like NGINX or Apache
    -httpd, or connecting over VPN (See [Secure Tracking Server](#tracking-auth) for more details).
    +MLflow 3.5.0+ includes built-in security middleware to protect against DNS rebinding
    +and CORS attacks. When using `--host 0.0.0.0`, configure the `--allowed-hosts` option
    +to specify which domains can access your server. See [Security Configuration](/ml/tracking/server/security)
    +for details.
     :::
     
     ## Logging to a Tracking Server \{#logging_to_a_tracking_server}
    @@ -225,14 +226,35 @@ Otherwise, all artifact requests will route to the MLflow Tracking server, defea
     
     ## Secure Tracking Server \{#tracking-auth}
     
    -The `--host` option exposes the service on all interfaces. If running a server in production, we
    -would recommend not exposing the built-in server broadly (as it is unauthenticated and unencrypted),
    -and instead putting it behind a reverse proxy like NGINX or Apache httpd, or connecting over VPN.
    +### Built-in Security Middleware
     
    -You can then pass authentication headers to MLflow using these environment variables .
    +MLflow 3.5.0+ includes security middleware that automatically protects against common web vulnerabilities:
    +
    +- **DNS Rebinding Protection**: Validates Host headers to prevent attacks on internal services
    +- **CORS Protection**: Controls which web applications can access your API
    +- **Clickjacking Prevention**: X-Frame-Options header controls iframe embedding
    +
    +Configure these features with simple command-line options:
    +
    +```bash
    +mlflow server --host 0.0.0.0 \
    +  --allowed-hosts "mlflow.company.com" \
    +  --cors-allowed-origins "https://app.company.com"
    +```
    +
    +For detailed configuration options, see [Security Configuration](/ml/tracking/server/security/configuration).
    +
    +### Authentication and Encryption
    +
    +For production deployments, we recommend using a reverse proxy (NGINX, Apache httpd) or VPN to add:
    +
    +- **TLS/HTTPS encryption** for secure communication
    +- **Authentication** via proxy authentication headers
    +
    +You can pass authentication headers to MLflow using these environment variables:
     
     - `MLFLOW_TRACKING_USERNAME` and `MLFLOW_TRACKING_PASSWORD` - username and password to use with HTTP
    -  Basic authentication. To use Basic authentication, you must set `both` environment variables .
    +  Basic authentication. To use Basic authentication, you must set `both` environment variables.
     - `MLFLOW_TRACKING_TOKEN` - token to use with HTTP Bearer authentication. Basic authentication takes precedence if set.
     - `MLFLOW_TRACKING_INSECURE_TLS` - If set to the literal `true`, MLflow does not verify the TLS connection,
       meaning it does not validate certificates or hostnames for `https://` tracking URIs. This flag is not recommended for
    @@ -247,6 +269,8 @@ You can then pass authentication headers to MLflow using these environment varia
       (see [requests main interface](https://requests.readthedocs.io/en/master/api)).
       This can be used to use a (self-signed) client certificate.
     
    +For notebook integration and UI embedding options, see [UI Access & Notebooks](/ml/tracking/server/security/ui-access).
    +
     ## Tracking Server versioning
     
     The version of MLflow running on the server can be found by querying the `/version` endpoint.
    
  • docs/docs/classic-ml/tracking/server/security/configuration.mdx+107 0 added
    @@ -0,0 +1,107 @@
    +---
    +sidebar_label: Configuration
    +---
    +
    +# Security Configuration
    +
    +## Requirements
    +
    +Security middleware features require the FastAPI-based tracking server (uvicorn), which is the default server in MLflow 3.5.0+. These features are not available when using `--gunicorn-opts` or `--waitress-opts`.
    +
    +## Configuration Options
    +
    +Security settings can be configured through CLI options or environment variables:
    +
    +| Setting              | CLI Option                      | Environment Variable                        | Default                |
    +| -------------------- | ------------------------------- | ------------------------------------------- | ---------------------- |
    +| **Allowed Hosts**    | `--allowed-hosts`               | `MLFLOW_SERVER_ALLOWED_HOSTS`               | localhost, private IPs |
    +| **CORS Origins**     | `--cors-allowed-origins`        | `MLFLOW_SERVER_CORS_ALLOWED_ORIGINS`        | localhost:\*           |
    +| **X-Frame-Options**  | `--x-frame-options`             | `MLFLOW_SERVER_X_FRAME_OPTIONS`             | SAMEORIGIN             |
    +| **Disable Security** | `--disable-security-middleware` | `MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE` | false                  |
    +
    +## --allowed-hosts
    +
    +Controls which Host headers the server accepts. This prevents DNS rebinding attacks by validating incoming requests:
    +
    +```bash
    +# Specific hosts
    +mlflow server --allowed-hosts "mlflow.company.com,192.168.1.100"
    +
    +# Wildcard patterns
    +mlflow server --allowed-hosts "*.company.com,192.168.*"
    +
    +# Allow all (not recommended)
    +mlflow server --allowed-hosts "*"
    +```
    +
    +## --cors-allowed-origins
    +
    +Specifies which web applications can make API requests from browsers:
    +
    +```bash
    +# Specific origins
    +mlflow server --cors-allowed-origins "https://app.company.com,https://notebook.company.com"
    +
    +# Wildcard for subdomains
    +mlflow server --cors-allowed-origins "https://*.company.com"
    +
    +# Allow all origins (development only)
    +mlflow server --cors-allowed-origins "*"
    +```
    +
    +## --x-frame-options
    +
    +Sets the X-Frame-Options header to control iframe embedding behavior:
    +
    +- `SAMEORIGIN` - Only same origin can embed (default)
    +- `DENY` - No embedding allowed
    +- `NONE` - Any site can embed
    +
    +```bash
    +# Allow cross-origin iframe embedding
    +mlflow server --x-frame-options NONE
    +```
    +
    +## --disable-security-middleware
    +
    +Completely disables security middleware. Use this only when security is handled by a reverse proxy or gateway:
    +
    +```bash
    +mlflow server --disable-security-middleware
    +```
    +
    +## Common Configurations
    +
    +Examples for typical deployment scenarios:
    +
    +### Local Development
    +
    +Default configuration works out of the box:
    +
    +```bash
    +mlflow server
    +```
    +
    +### Remote Access
    +
    +Allow connections from specific hosts:
    +
    +```bash
    +mlflow server --host 0.0.0.0 --allowed-hosts "mlflow.internal:5000,localhost:*"
    +```
    +
    +### CORS for Web Apps
    +
    +Enable browser-based applications to access the API:
    +
    +```bash
    +mlflow server --cors-allowed-origins "https://notebook.internal"
    +```
    +
    +### Allow iframe Embedding
    +
    +Enable embedding the UI in other applications:
    +
    +```bash
    +mlflow server --x-frame-options NONE
    +```
    
  • docs/docs/classic-ml/tracking/server/security/index.mdx+92 0 added
    @@ -0,0 +1,92 @@
    +---
    +sidebar_label: Server Security
    +---
    +
    +import TilesGrid from "@site/src/components/TilesGrid";
    +import TileCard from "@site/src/components/TileCard";
    +import { Shield, Settings, Monitor, Wrench } from "lucide-react";
    +
    +# MLflow Tracking Server Security
    +
    +MLflow 3.5.0+ includes security middleware to protect against DNS rebinding, CORS attacks, and clickjacking. These features are available with the default FastAPI-based tracking server (uvicorn).
    +
    +## Quick Start
    +
    +Configure security based on your deployment scenario:
    +
    +```bash
    +# Local development (secure by default)
    +mlflow server
    +
    +# Team server
    +mlflow server --host 0.0.0.0 \
    +  --allowed-hosts "mlflow.internal:5000" \
    +  --cors-allowed-origins "https://notebook.internal"
    +
    +# Production
    +export MLFLOW_SERVER_ALLOWED_HOSTS="mlflow.company.com"
    +export MLFLOW_SERVER_CORS_ALLOWED_ORIGINS="https://app.company.com"
    +mlflow server --host 127.0.0.1
    +```
    +
    +## Security Features
    +
    +The tracking server includes these built-in protections:
    +
    +<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
    +  <div className="border rounded-lg p-4">
    +    <Shield className="w-8 h-8 mb-2 text-blue-600" />
    +    <h3 className="font-semibold mb-2">DNS Rebinding Protection</h3>
    +    <p className="text-sm text-gray-600">Validates Host headers to prevent attacks on internal services</p>
    +  </div>
    +  <div className="border rounded-lg p-4">
    +    <Shield className="w-8 h-8 mb-2 text-blue-600" />
    +    <h3 className="font-semibold mb-2">CORS Protection</h3>
    +    <p className="text-sm text-gray-600">Controls which web apps can access your MLflow API</p>
    +  </div>
    +  <div className="border rounded-lg p-4">
    +    <Shield className="w-8 h-8 mb-2 text-blue-600" />
    +    <h3 className="font-semibold mb-2">Clickjacking Prevention</h3>
    +    <p className="text-sm text-gray-600">X-Frame-Options header controls iframe embedding</p>
    +  </div>
    +  <div className="border rounded-lg p-4">
    +    <Shield className="w-8 h-8 mb-2 text-blue-600" />
    +    <h3 className="font-semibold mb-2">Security Headers</h3>
    +    <p className="text-sm text-gray-600">Automatic headers prevent MIME sniffing and XSS</p>
    +  </div>
    +</div>
    +
    +## Documentation
    +
    +Learn how to configure and use security features:
    +
    +<TilesGrid>
    +  <TileCard
    +    icon={Settings}
    +    iconSize={48}
    +    title="Configuration"
    +    description="Configure security settings"
    +    href="/ml/tracking/server/security/configuration"
    +    linkText="Configure →"
    +    containerHeight={64}
    +  />
    +  <TileCard
    +    icon={Monitor}
    +    iconSize={48}
    +    title="UI Access"
    +    description="Enable remote access and iframe embedding"
    +    href="/ml/tracking/server/security/ui-access"
    +    linkText="Setup access →"
    +    containerHeight={64}
    +  />
    +</TilesGrid>
    +
    +## Default Behavior
    +
    +Security defaults are designed to be safe for local development while requiring explicit configuration for production:
    +
    +| Feature              | Default                 | Production                   |
    +| -------------------- | ----------------------- | ---------------------------- |
    +| **Host Validation**  | Localhost + private IPs | Configure specific domains   |
    +| **CORS Origins**     | Any localhost origin    | Specific application origins |
    +| **iframe Embedding** | SAMEORIGIN              | SAMEORIGIN or CSP            |
    
  • docs/docs/classic-ml/tracking/server/security/ui-access.mdx+97 0 added
    @@ -0,0 +1,97 @@
    +---
    +sidebar_label: UI Access & Notebooks
    +---
    +
    +# UI Access & Notebook Integration
    +
    +## Remote SDK Access
    +
    +The MLflow Python SDK connects to remote tracking servers without special configuration:
    +
    +```python
    +import mlflow
    +
    +# Connect to remote server
    +mlflow.set_tracking_uri("http://mlflow.company.com:5000")
    +
    +with mlflow.start_run():
    +    mlflow.log_param("alpha", 0.5)
    +    mlflow.log_metric("rmse", 0.1)
    +```
    +
    +## Jupyter Notebook Integration
    +
    +Jupyter notebooks can embed MLflow UI components using iframes.
    +
    +### Cross-Origin Notebooks
    +
    +When notebooks run on a different domain than your MLflow server, configure CORS and frame options:
    +
    +```bash
    +# Allow embedding from notebook domain
    +mlflow server --host 0.0.0.0 \
    +  --x-frame-options NONE \
    +  --cors-allowed-origins "https://jupyter.company.com"
    +```
    +
    +### Manual iframe Embedding
    +
    +Embed specific MLflow views directly in notebook cells:
    +
    +```python
    +from IPython.display import IFrame
    +
    +# Embed MLflow UI
    +IFrame(src="http://mlflow.company.com:5000", width=1000, height=600)
    +```
    +
    +## Embedding in Web Applications
    +
    +Web applications can embed the MLflow UI using iframes.
    +
    +### React Application
    +
    +Create a component to display MLflow content:
    +
    +```jsx
    +function MLflowDashboard() {
    +  return (
    +    <iframe
    +      src="http://mlflow.company.com:5000/experiments/1"
    +      style={{ width: '100%', height: '800px' }}
    +      title="MLflow"
    +    />
    +  );
    +}
    +```
    +
    +Configure the MLflow server to accept requests from your React app:
    +
    +```bash
    +mlflow server --host 0.0.0.0 \
    +  --x-frame-options NONE \
    +  --cors-allowed-origins "http://localhost:3000,https://app.company.com"
    +```
    +
    +## Testing iframe Embedding
    +
    +Use this HTML file to verify iframe configuration works correctly:
    +
    +```html
    +<!DOCTYPE html>
    +<html>
    +<head>
    +    <title>MLflow iframe Test</title>
    +</head>
    +<body>
    +    <h1>MLflow Embedding Test</h1>
    +    <iframe
    +        src="http://localhost:5000"
    +        style="width: 100%; height: 600px; border: 1px solid #ccc;"
    +        onload="document.getElementById('status').innerHTML = '✅ Loaded'"
    +        onerror="document.getElementById('status').innerHTML = '❌ Failed'">
    +    </iframe>
    +    <div id="status">Loading...</div>
    +</body>
    +</html>
    +```
    
  • docs/sidebarsClassicML.ts+18 0 modified
    @@ -532,6 +532,24 @@ const sidebarsClassicML: SidebarsConfig = {
                   type: 'category',
                   label: 'Tracking Server',
                   items: [
    +                {
    +                  type: 'category',
    +                  label: 'Server Security',
    +                  link: {
    +                    type: 'doc',
    +                    id: 'tracking/server/security/index',
    +                  },
    +                  items: [
    +                    {
    +                      type: 'doc',
    +                      id: 'tracking/server/security/configuration',
    +                    },
    +                    {
    +                      type: 'doc',
    +                      id: 'tracking/server/security/ui-access',
    +                    },
    +                  ],
    +                },
                     {
                       type: 'doc',
                       id: 'tracking/artifact-stores/index',
    
  • docs/sidebars.ts+18 0 modified
    @@ -348,6 +348,24 @@ const sidebars: SidebarsConfig = {
                       type: 'doc',
                       id: 'tracking/backend-stores/index',
                     },
    +                {
    +                  type: 'category',
    +                  label: 'Server Security',
    +                  link: {
    +                    type: 'doc',
    +                    id: 'tracking/server/security/index',
    +                  },
    +                  items: [
    +                    {
    +                      type: 'doc',
    +                      id: 'tracking/server/security/configuration',
    +                    },
    +                    {
    +                      type: 'doc',
    +                      id: 'tracking/server/security/ui-access',
    +                    },
    +                  ],
    +                },
                     {
                       type: 'category',
                       label: 'Tutorials',
    
  • examples/mlflow_artifacts/docker-compose.yml+2 0 modified
    @@ -49,6 +49,7 @@ services:
           AWS_ACCESS_KEY_ID: "user"
           AWS_SECRET_ACCESS_KEY: "password"
           MLFLOW_SERVER_ENABLE_JOB_EXECUTION: "false"
    +      MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE: "true"
         command: >
           mlflow server
           --host 0.0.0.0
    @@ -79,6 +80,7 @@ services:
           - "5000:5000"
         environment:
           MLFLOW_SERVER_ENABLE_JOB_EXECUTION: "false"
    +      MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE: "true"
         command: >
           mlflow server
           --host 0.0.0.0
    
  • mlflow/cli/__init__.py+105 4 modified
    @@ -276,14 +276,23 @@ def _user_args_to_dict(arguments, argument_type="P"):
         return user_dict
     
     
    -def _validate_server_args(gunicorn_opts=None, workers=None, waitress_opts=None, uvicorn_opts=None):
    +def _validate_server_args(
    +    ctx=None,
    +    gunicorn_opts=None,
    +    workers=None,
    +    waitress_opts=None,
    +    uvicorn_opts=None,
    +    allowed_hosts=None,
    +    cors_allowed_origins=None,
    +    x_frame_options=None,
    +    disable_security_middleware=None,
    +):
         if sys.platform == "win32":
             if gunicorn_opts is not None:
                 raise NotImplementedError(
                     "gunicorn is not supported on Windows, cannot specify --gunicorn-opts"
                 )
     
    -    # Check for conflicting options
         num_server_opts_specified = sum(
             1 for opt in [gunicorn_opts, waitress_opts, uvicorn_opts] if opt is not None
         )
    @@ -293,6 +302,33 @@ def _validate_server_args(gunicorn_opts=None, workers=None, waitress_opts=None,
                 "'--gunicorn-opts', '--waitress-opts', or '--uvicorn-opts'."
             )
     
    +    using_flask_only = gunicorn_opts is not None or waitress_opts is not None
    +    # NB: Only check for security params that are explicitly passed via CLI (not env vars)
    +    # This allows Docker containers to set env vars while using gunicorn
    +    from click.core import ParameterSource
    +
    +    security_params_specified = False
    +    if ctx:
    +        security_params_specified = any(
    +            [
    +                ctx.get_parameter_source("allowed_hosts") == ParameterSource.COMMANDLINE,
    +                ctx.get_parameter_source("cors_allowed_origins") == ParameterSource.COMMANDLINE,
    +                (
    +                    ctx.get_parameter_source("disable_security_middleware")
    +                    == ParameterSource.COMMANDLINE
    +                ),
    +            ]
    +        )
    +
    +    if using_flask_only and security_params_specified:
    +        raise click.UsageError(
    +            "Security middleware parameters (--allowed-hosts, --cors-allowed-origins, "
    +            "--disable-security-middleware) are only supported with "
    +            "the default uvicorn server. They cannot be used with --gunicorn-opts or "
    +            "--waitress-opts. To use security features, run without specifying a server "
    +            "option (uses uvicorn by default) or explicitly use --uvicorn-opts."
    +        )
    +
     
     def _validate_static_prefix(ctx, param, value):
         """
    @@ -359,6 +395,10 @@ def _validate_static_prefix(ctx, param, value):
     @cli_args.HOST
     @cli_args.PORT
     @cli_args.WORKERS
    +@cli_args.ALLOWED_HOSTS
    +@cli_args.CORS_ALLOWED_ORIGINS
    +@cli_args.DISABLE_SECURITY_MIDDLEWARE
    +@cli_args.X_FRAME_OPTIONS
     @click.option(
         "--static-prefix",
         envvar="MLFLOW_STATIC_PREFIX",
    @@ -422,6 +462,10 @@ def server(
         host,
         port,
         workers,
    +    allowed_hosts,
    +    cors_allowed_origins,
    +    disable_security_middleware,
    +    x_frame_options,
         static_prefix,
         gunicorn_opts,
         waitress_opts,
    @@ -431,12 +475,15 @@ def server(
         uvicorn_opts,
     ):
         """
    -    Run the MLflow tracking server.
    +    Run the MLflow tracking server with built-in security middleware.
     
         The server listens on http://localhost:5000 by default and only accepts connections
         from the local machine. To let the server accept connections from other machines, you will need
         to pass ``--host 0.0.0.0`` to listen on all network interfaces
         (or a specific interface address).
    +
    +    See https://mlflow.org/docs/latest/tracking/server-security.html for detailed documentation
    +    and guidance on security configurations for the MLflow tracking server.
         """
         from mlflow.server import _run_server
         from mlflow.server.handlers import initialize_backend_stores
    @@ -457,16 +504,43 @@ def server(
                     "is only supported for the default MLflow tracking server."
                 )
     
    -        # In dev mode, use uvicorn with reload and debug logging
             uvicorn_opts = "--reload --log-level debug"
     
         _validate_server_args(
    +        ctx=ctx,
             gunicorn_opts=gunicorn_opts,
             workers=workers,
             waitress_opts=waitress_opts,
             uvicorn_opts=uvicorn_opts,
    +        allowed_hosts=allowed_hosts,
    +        cors_allowed_origins=cors_allowed_origins,
    +        x_frame_options=x_frame_options,
    +        disable_security_middleware=disable_security_middleware,
         )
     
    +    if disable_security_middleware:
    +        os.environ["MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE"] = "true"
    +    else:
    +        if allowed_hosts:
    +            os.environ["MLFLOW_SERVER_ALLOWED_HOSTS"] = allowed_hosts
    +            if allowed_hosts == "*":
    +                click.echo(
    +                    "WARNING: Accepting ALL hosts. "
    +                    "This may leave the server vulnerable to DNS rebinding attacks."
    +                )
    +
    +        if cors_allowed_origins:
    +            os.environ["MLFLOW_SERVER_CORS_ALLOWED_ORIGINS"] = cors_allowed_origins
    +            if cors_allowed_origins == "*":
    +                click.echo(
    +                    "WARNING: Allowing ALL origins for CORS. "
    +                    "This allows ANY website to access your MLflow data. "
    +                    "This configuration is only recommended for local development."
    +                )
    +
    +        if x_frame_options:
    +            os.environ["MLFLOW_SERVER_X_FRAME_OPTIONS"] = x_frame_options
    +
         # Ensure that both backend_store_uri and default_artifact_uri are set correctly.
         if not backend_store_uri:
             backend_store_uri = DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH
    @@ -487,6 +561,33 @@ def server(
             _logger.exception(e)
             sys.exit(1)
     
    +    if disable_security_middleware:
    +        click.echo(
    +            "[MLflow] WARNING: Security middleware is DISABLED. "
    +            "Your MLflow server is vulnerable to various attacks.",
    +            err=True,
    +        )
    +    elif not allowed_hosts and not cors_allowed_origins:
    +        click.echo(
    +            "[MLflow] Security middleware enabled with default settings (localhost-only). "
    +            "To allow connections from other hosts, use --host 0.0.0.0 and configure "
    +            "--allowed-hosts and --cors-allowed-origins.",
    +            err=True,
    +        )
    +    else:
    +        parts = ["[MLflow] Security middleware enabled"]
    +        if allowed_hosts:
    +            hosts_list = allowed_hosts.split(",")[:3]
    +            if len(allowed_hosts.split(",")) > 3:
    +                hosts_list.append(f"and {len(allowed_hosts.split(',')) - 3} more")
    +            parts.append(f"Allowed hosts: {', '.join(hosts_list)}")
    +        if cors_allowed_origins:
    +            origins_list = cors_allowed_origins.split(",")[:3]
    +            if len(cors_allowed_origins.split(",")) > 3:
    +                origins_list.append(f"and {len(cors_allowed_origins.split(',')) - 3} more")
    +            parts.append(f"CORS origins: {', '.join(origins_list)}")
    +        click.echo(". ".join(parts) + ".", err=True)
    +
         try:
             _run_server(
                 file_store_path=backend_store_uri,
    
  • mlflow/environment_variables.py+30 0 modified
    @@ -783,6 +783,36 @@ def get(self):
     #: in the UI signup page when running the app with basic authentication enabled
     MLFLOW_FLASK_SERVER_SECRET_KEY = _EnvironmentVariable("MLFLOW_FLASK_SERVER_SECRET_KEY", str, None)
     
    +#: (MLflow 3.5.0+) Comma-separated list of allowed CORS origins for the MLflow server.
    +#: Example: "http://localhost:3000,https://app.example.com"
    +#: Use "*" to allow ALL origins (DANGEROUS - only use for development!).
    +#: (default: ``None`` - localhost origins only)
    +MLFLOW_SERVER_CORS_ALLOWED_ORIGINS = _EnvironmentVariable(
    +    "MLFLOW_SERVER_CORS_ALLOWED_ORIGINS", str, None
    +)
    +
    +#: (MLflow 3.5.0+) Comma-separated list of allowed Host headers for the MLflow server.
    +#: Example: "mlflow.company.com,mlflow.internal:5000"
    +#: Use "*" to allow ALL hosts (not recommended for production).
    +#: If not set, defaults to localhost variants and private IP ranges.
    +#: (default: ``None`` - localhost and private IP ranges)
    +MLFLOW_SERVER_ALLOWED_HOSTS = _EnvironmentVariable("MLFLOW_SERVER_ALLOWED_HOSTS", str, None)
    +
    +#: (MLflow 3.5.0+) Disable all security middleware (DANGEROUS - only use for testing!).
    +#: Set to "true" to disable security headers, CORS protection, and host validation.
    +#: (default: ``"false"``)
    +MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE = _EnvironmentVariable(
    +    "MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE", str, "false"
    +)
    +
    +#: (MLflow 3.5.0+) X-Frame-Options header value for clickjacking protection.
    +#: Options: "SAMEORIGIN" (default), "DENY", or "NONE" (disable).
    +#: Set to "NONE" to allow embedding MLflow UI in iframes from different origins.
    +#: (default: ``"SAMEORIGIN"``)
    +MLFLOW_SERVER_X_FRAME_OPTIONS = _EnvironmentVariable(
    +    "MLFLOW_SERVER_X_FRAME_OPTIONS", str, "SAMEORIGIN"
    +)
    +
     #: Specifies the max length (in chars) of an experiment's artifact location.
     #: The default is 2048.
     MLFLOW_ARTIFACT_LOCATION_MAX_LENGTH = _EnvironmentVariable(
    
  • mlflow/server/AGENTS.md+181 0 added
    @@ -0,0 +1,181 @@
    +# MLflow Tracking Server Security Configuration Guide
    +
    +This document provides a quick reference for AI assistants to understand MLflow tracking server security options and configurations.
    +
    +## Overview
    +
    +The MLflow tracking server includes built-in security middleware to protect against common web vulnerabilities:
    +
    +- **DNS rebinding attacks** - via Host header validation
    +- **Cross-Origin Resource Sharing (CORS) attacks** - via origin validation
    +- **Clickjacking** - via X-Frame-Options header
    +
    +## Starting the Server
    +
    +```bash
    +# Basic start (localhost-only, secure by default)
    +mlflow server
    +
    +# Allow connections from other machines
    +mlflow server --host 0.0.0.0
    +
    +# Custom port
    +mlflow server --port 8080
    +```
    +
    +## Security Configuration Options
    +
    +### 1. Host Header Validation (`--allowed-hosts`)
    +
    +Prevents DNS rebinding attacks by validating the Host header in incoming requests.
    +
    +```bash
    +# Allow specific hosts
    +mlflow server --allowed-hosts "mlflow.company.com,10.0.0.100:5000"
    +
    +# Allow hosts with wildcards
    +mlflow server --allowed-hosts "mlflow.company.com,192.168.*,app-*.internal.com"
    +
    +# DANGEROUS: Allow all hosts (not recommended for production)
    +mlflow server --allowed-hosts "*"
    +```
    +
    +**Default behavior**: Allows localhost (all ports) and private IP ranges (10._, 192.168._, 172.16-31.\*).
    +
    +### 2. CORS Origin Validation (`--cors-allowed-origins`)
    +
    +Controls which web applications can make requests to your MLflow server.
    +
    +```bash
    +# Allow specific origins
    +mlflow server --cors-allowed-origins "https://app.company.com,https://notebook.company.com"
    +
    +# DANGEROUS: Allow all origins (only for development)
    +mlflow server --cors-allowed-origins "*"
    +```
    +
    +**Default behavior**: Allows `http://localhost:*, http://127.0.0.1:*, http://[::1]:*` (all ports).
    +
    +### 3. Clickjacking Protection (`--x-frame-options`)
    +
    +Controls whether the MLflow UI can be embedded in iframes.
    +
    +```bash
    +# Default: Same origin only
    +mlflow server --x-frame-options SAMEORIGIN
    +
    +# Deny all iframe embedding
    +mlflow server --x-frame-options DENY
    +
    +# Allow iframe embedding from anywhere (not recommended)
    +mlflow server --x-frame-options NONE
    +```
    +
    +### 4. Disable Security Middleware (`--disable-security-middleware`)
    +
    +**DANGEROUS**: Completely disables all security protections.
    +
    +```bash
    +# Only for testing - removes all security protections
    +mlflow server --disable-security-middleware
    +```
    +
    +## Common Configuration Scenarios
    +
    +### Local Development (Default)
    +
    +```bash
    +mlflow server
    +# Security: Enabled (localhost-only)
    +# Access: Only from local machine
    +```
    +
    +### Team Development Server
    +
    +```bash
    +mlflow server \
    +  --host 0.0.0.0 \
    +  --allowed-hosts "mlflow.dev.company.com,192.168.*" \
    +  --cors-allowed-origins "https://notebook.dev.company.com"
    +```
    +
    +### Production Server
    +
    +```bash
    +mlflow server \
    +  --host 0.0.0.0 \
    +  --allowed-hosts "mlflow.prod.company.com" \
    +  --cors-allowed-origins "https://app.prod.company.com,https://notebook.prod.company.com" \
    +  --x-frame-options DENY
    +```
    +
    +### Docker Container Setup
    +
    +```bash
    +# In docker-compose.yml, set environment variables:
    +environment:
    +  MLFLOW_SERVER_ALLOWED_HOSTS: "tracking-server:5000,localhost:5000,127.0.0.1:5000"
    +  MLFLOW_SERVER_CORS_ALLOWED_ORIGINS: "http://frontend:3000"
    +```
    +
    +## Environment Variables
    +
    +All CLI options can be set via environment variables:
    +
    +- `MLFLOW_SERVER_ALLOWED_HOSTS` - Comma-separated list of allowed hosts
    +- `MLFLOW_SERVER_CORS_ALLOWED_ORIGINS` - Comma-separated list of allowed CORS origins
    +- `MLFLOW_SERVER_X_FRAME_OPTIONS` - Clickjacking protection setting
    +- `MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE` - Set to "true" to disable security
    +
    +## Security Messages
    +
    +When starting the server, users see one of these messages:
    +
    +1. **Default configuration**:
    +
    +   ```bash
    +   [MLflow] Security middleware enabled with default settings (localhost-only).
    +   To allow connections from other hosts, use --host 0.0.0.0 and configure
    +   --allowed-hosts and --cors-allowed-origins.
    +   ```
    +
    +2. **Custom configuration**:
    +
    +   ```bash
    +   [MLflow] Security middleware enabled. Allowed hosts: mlflow.company.com, 192.168.*.
    +   CORS origins: https://app.company.com.
    +   ```
    +
    +3. **Security disabled**:
    +
    +   ```bash
    +   [MLflow] WARNING: Security middleware is DISABLED. Your MLflow server is vulnerable to various attacks.
    +   ```
    +
    +## Implementation Details
    +
    +- Security middleware is implemented in:
    +  - Flask: `mlflow/server/security.py`
    +  - FastAPI: `mlflow/server/fastapi_security.py`
    +- Configuration messages displayed in: `mlflow/cli/__init__.py` (server function)
    +- Security is enabled by default unless explicitly disabled
    +
    +## Testing Security Configuration
    +
    +```bash
    +# Test Host header validation
    +curl -H "Host: evil.com" http://localhost:5000/api/2.0/mlflow/experiments/search
    +# Should return: 400 Bad Request - Invalid Host header
    +
    +# Test CORS
    +curl -H "Origin: https://evil.com" http://localhost:5000/api/2.0/mlflow/experiments/search
    +# Should not include Access-Control-Allow-Origin header for unauthorized origin
    +```
    +
    +## Important Notes
    +
    +1. **Security by default**: The server is secure by default, only accepting localhost connections
    +2. **Host validation**: When using `--host 0.0.0.0`, always configure `--allowed-hosts`
    +3. **CORS in production**: Always specify exact origins, never use "\*" in production
    +4. **Docker networking**: Container names (e.g., "tracking-server") must be in allowed hosts
    +5. **Private IPs**: Default configuration allows private IP ranges for development convenience
    
  • mlflow/server/fastapi_app.py+4 0 modified
    @@ -11,6 +11,7 @@
     from flask import Flask
     
     from mlflow.server import app as flask_app
    +from mlflow.server.fastapi_security import init_fastapi_security
     from mlflow.server.job_api import job_api_router
     from mlflow.server.otel_api import otel_router
     from mlflow.version import VERSION
    @@ -35,6 +36,9 @@ def create_fastapi_app(flask_app: Flask = flask_app):
             openapi_url=None,
         )
     
    +    # Initialize security middleware BEFORE adding routes
    +    init_fastapi_security(fastapi_app)
    +
         # Include OpenTelemetry API router BEFORE mounting Flask app
         # This ensures FastAPI routes take precedence over the catch-all Flask mount
         fastapi_app.include_router(otel_router)
    
  • mlflow/server/fastapi_security.py+182 0 added
    @@ -0,0 +1,182 @@
    +import logging
    +from http import HTTPStatus
    +
    +from fastapi import FastAPI
    +from fastapi.middleware.cors import CORSMiddleware
    +from starlette.types import ASGIApp
    +
    +from mlflow.environment_variables import (
    +    MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE,
    +    MLFLOW_SERVER_X_FRAME_OPTIONS,
    +)
    +from mlflow.server.security_utils import (
    +    CORS_BLOCKED_MSG,
    +    HEALTH_ENDPOINTS,
    +    INVALID_HOST_MSG,
    +    get_allowed_hosts_from_env,
    +    get_allowed_origins_from_env,
    +    get_default_allowed_hosts,
    +    is_allowed_host_header,
    +    is_api_endpoint,
    +    should_block_cors_request,
    +)
    +
    +_logger = logging.getLogger(__name__)
    +
    +
    +class HostValidationMiddleware:
    +    """Middleware to validate Host headers using fnmatch patterns."""
    +
    +    def __init__(self, app: ASGIApp, allowed_hosts: list[str]):
    +        self.app = app
    +        self.allowed_hosts = allowed_hosts
    +
    +    async def __call__(self, scope, receive, send):
    +        if scope["type"] != "http":
    +            return await self.app(scope, receive, send)
    +
    +        if scope["path"] in HEALTH_ENDPOINTS:
    +            return await self.app(scope, receive, send)
    +
    +        headers = dict(scope.get("headers", []))
    +        host = headers.get(b"host", b"").decode("utf-8")
    +
    +        if not is_allowed_host_header(self.allowed_hosts, host):
    +            _logger.warning(f"Rejected request with invalid Host header: {host}")
    +
    +            async def send_403(message):
    +                if message["type"] == "http.response.start":
    +                    message["status"] = 403
    +                    message["headers"] = [(b"content-type", b"text/plain")]
    +                await send(message)
    +
    +            await send_403({"type": "http.response.start", "status": 403, "headers": []})
    +            await send({"type": "http.response.body", "body": INVALID_HOST_MSG.encode()})
    +            return
    +
    +        return await self.app(scope, receive, send)
    +
    +
    +class SecurityHeadersMiddleware:
    +    """Middleware to add security headers to all responses."""
    +
    +    def __init__(self, app: ASGIApp):
    +        self.app = app
    +        self.x_frame_options = MLFLOW_SERVER_X_FRAME_OPTIONS.get()
    +
    +    async def __call__(self, scope, receive, send):
    +        if scope["type"] != "http":
    +            return await self.app(scope, receive, send)
    +
    +        async def send_wrapper(message):
    +            if message["type"] == "http.response.start":
    +                headers = dict(message.get("headers", []))
    +                headers[b"x-content-type-options"] = b"nosniff"
    +
    +                if self.x_frame_options and self.x_frame_options.upper() != "NONE":
    +                    headers[b"x-frame-options"] = self.x_frame_options.upper().encode()
    +
    +                if (
    +                    scope["method"] == "OPTIONS"
    +                    and message.get("status") == 200
    +                    and is_api_endpoint(scope["path"])
    +                ):
    +                    message["status"] = HTTPStatus.NO_CONTENT
    +
    +                message["headers"] = list(headers.items())
    +            await send(message)
    +
    +        await self.app(scope, receive, send_wrapper)
    +
    +
    +class CORSBlockingMiddleware:
    +    """Middleware to actively block cross-origin state-changing requests."""
    +
    +    def __init__(self, app: ASGIApp, allowed_origins: list[str]):
    +        self.app = app
    +        self.allowed_origins = allowed_origins
    +
    +    async def __call__(self, scope, receive, send):
    +        if scope["type"] != "http":
    +            return await self.app(scope, receive, send)
    +
    +        if not is_api_endpoint(scope["path"]):
    +            return await self.app(scope, receive, send)
    +
    +        method = scope["method"]
    +        headers = dict(scope["headers"])
    +        origin = headers.get(b"origin", b"").decode("utf-8")
    +
    +        if should_block_cors_request(origin, method, self.allowed_origins):
    +            _logger.warning(f"Blocked cross-origin request from {origin}")
    +            await send(
    +                {
    +                    "type": "http.response.start",
    +                    "status": HTTPStatus.FORBIDDEN,
    +                    "headers": [[b"content-type", b"text/plain"]],
    +                }
    +            )
    +            await send(
    +                {
    +                    "type": "http.response.body",
    +                    "body": CORS_BLOCKED_MSG.encode(),
    +                }
    +            )
    +            return
    +
    +        await self.app(scope, receive, send)
    +
    +
    +def get_allowed_hosts() -> list[str]:
    +    """Get list of allowed hosts from environment or defaults."""
    +    return get_allowed_hosts_from_env() or get_default_allowed_hosts()
    +
    +
    +def get_allowed_origins() -> list[str]:
    +    """Get list of allowed CORS origins from environment or defaults."""
    +    return get_allowed_origins_from_env() or []
    +
    +
    +def init_fastapi_security(app: FastAPI) -> None:
    +    """
    +    Initialize security middleware for FastAPI application.
    +
    +    This configures:
    +    - Host header validation (DNS rebinding protection) via TrustedHostMiddleware
    +    - CORS protection via CORSMiddleware
    +    - Security headers via custom middleware
    +
    +    Args:
    +        app: FastAPI application instance.
    +    """
    +    if MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE.get() == "true":
    +        return
    +
    +    app.add_middleware(SecurityHeadersMiddleware)
    +
    +    allowed_origins = get_allowed_origins()
    +
    +    if allowed_origins and "*" in allowed_origins:
    +        app.add_middleware(
    +            CORSMiddleware,
    +            allow_origins=["*"],
    +            allow_credentials=True,
    +            allow_methods=["*"],
    +            allow_headers=["*"],
    +            expose_headers=["*"],
    +        )
    +    else:
    +        app.add_middleware(CORSBlockingMiddleware, allowed_origins=allowed_origins)
    +        app.add_middleware(
    +            CORSMiddleware,
    +            allow_origins=["*"],
    +            allow_credentials=True,
    +            allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
    +            allow_headers=["*"],
    +            expose_headers=["*"],
    +        )
    +
    +    allowed_hosts = get_allowed_hosts()
    +
    +    if allowed_hosts and "*" not in allowed_hosts:
    +        app.add_middleware(HostValidationMiddleware, allowed_hosts=allowed_hosts)
    
  • mlflow/server/__init__.py+12 0 modified
    @@ -57,6 +57,18 @@
     app = Flask(__name__, static_folder=REL_STATIC_DIR)
     IS_FLASK_V1 = Version(importlib.metadata.version("flask")) < Version("2.0")
     
    +is_running_as_server = (
    +    "gunicorn" in sys.modules
    +    or "uvicorn" in sys.modules
    +    or "waitress" in sys.modules
    +    or os.getenv(BACKEND_STORE_URI_ENV_VAR)
    +    or os.getenv(SERVE_ARTIFACTS_ENV_VAR)
    +)
    +
    +if is_running_as_server:
    +    from mlflow.server import security
    +
    +    security.init_security_middleware(app)
     
     for http_path, handler, methods in handlers.get_endpoints():
         app.add_url_rule(http_path, handler.__name__, handler, methods=methods)
    
  • mlflow/server/js/nohup.out+2540 0 added
  • mlflow/server/security.py+111 0 added
    @@ -0,0 +1,111 @@
    +import logging
    +from http import HTTPStatus
    +
    +from flask import Flask, Response, request
    +from flask_cors import CORS
    +
    +from mlflow.environment_variables import (
    +    MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE,
    +    MLFLOW_SERVER_X_FRAME_OPTIONS,
    +)
    +from mlflow.server.security_utils import (
    +    CORS_BLOCKED_MSG,
    +    HEALTH_ENDPOINTS,
    +    INVALID_HOST_MSG,
    +    LOCALHOST_ORIGIN_PATTERNS,
    +    get_allowed_hosts_from_env,
    +    get_allowed_origins_from_env,
    +    get_default_allowed_hosts,
    +    is_allowed_host_header,
    +    is_api_endpoint,
    +    should_block_cors_request,
    +)
    +
    +_logger = logging.getLogger(__name__)
    +
    +
    +def get_allowed_hosts() -> list[str]:
    +    """Get list of allowed hosts from environment or defaults."""
    +    return get_allowed_hosts_from_env() or get_default_allowed_hosts()
    +
    +
    +def get_allowed_origins() -> list[str]:
    +    """Get list of allowed CORS origins from environment or defaults."""
    +    return get_allowed_origins_from_env() or []
    +
    +
    +def init_security_middleware(app: Flask) -> None:
    +    """
    +    Initialize security middleware for Flask application.
    +
    +    This configures:
    +    - Host header validation (DNS rebinding protection)
    +    - CORS protection via Flask-CORS
    +    - Security headers
    +
    +    Args:
    +        app: Flask application instance.
    +    """
    +    if MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE.get() == "true":
    +        return
    +
    +    allowed_origins = get_allowed_origins()
    +    allowed_hosts = get_allowed_hosts()
    +    x_frame_options = MLFLOW_SERVER_X_FRAME_OPTIONS.get()
    +
    +    if allowed_origins and "*" in allowed_origins:
    +        CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
    +    else:
    +        cors_origins = (allowed_origins or []) + LOCALHOST_ORIGIN_PATTERNS
    +        CORS(
    +            app,
    +            resources={r"/*": {"origins": cors_origins}},
    +            supports_credentials=True,
    +            methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
    +        )
    +
    +    if allowed_hosts and "*" not in allowed_hosts:
    +
    +        @app.before_request
    +        def validate_host():
    +            if request.path in HEALTH_ENDPOINTS:
    +                return None
    +
    +            if not is_allowed_host_header(allowed_hosts, host := request.headers.get("Host")):
    +                _logger.warning(f"Rejected request with invalid Host header: {host}")
    +                return Response(
    +                    INVALID_HOST_MSG, status=HTTPStatus.FORBIDDEN, mimetype="text/plain"
    +                )
    +            return None
    +
    +    if not (allowed_origins and "*" in allowed_origins):
    +
    +        @app.before_request
    +        def block_cross_origin_state_changes():
    +            if not is_api_endpoint(request.path):
    +                return None
    +
    +            origin = request.headers.get("Origin")
    +            if should_block_cors_request(origin, request.method, allowed_origins):
    +                _logger.warning(f"Blocked cross-origin request from {origin}")
    +                return Response(
    +                    CORS_BLOCKED_MSG, status=HTTPStatus.FORBIDDEN, mimetype="text/plain"
    +                )
    +            return None
    +
    +    @app.after_request
    +    def add_security_headers(response: Response) -> Response:
    +        response.headers["X-Content-Type-Options"] = "nosniff"
    +
    +        if x_frame_options and x_frame_options.upper() != "NONE":
    +            response.headers["X-Frame-Options"] = x_frame_options.upper()
    +
    +        if (
    +            request.method == "OPTIONS"
    +            and response.status_code == 200
    +            and is_api_endpoint(request.path)
    +        ):
    +            response.status_code = HTTPStatus.NO_CONTENT
    +            response.data = b""
    +
    +        return response
    
  • mlflow/server/security_utils.py+154 0 added
    @@ -0,0 +1,154 @@
    +"""
    +Shared security utilities for MLflow server middleware.
    +
    +This module contains common functions used by both Flask and FastAPI
    +security implementations.
    +"""
    +
    +import fnmatch
    +from urllib.parse import urlparse
    +
    +from mlflow.environment_variables import (
    +    MLFLOW_SERVER_ALLOWED_HOSTS,
    +    MLFLOW_SERVER_CORS_ALLOWED_ORIGINS,
    +)
    +
    +# Security response messages
    +INVALID_HOST_MSG = "Invalid Host header - possible DNS rebinding attack detected"
    +CORS_BLOCKED_MSG = "Cross-origin request blocked"
    +
    +# HTTP methods that modify state
    +STATE_CHANGING_METHODS = ["POST", "PUT", "DELETE", "PATCH"]
    +
    +# Paths exempt from host validation
    +HEALTH_ENDPOINTS = ["/health", "/version"]
    +
    +# API path prefix for MLflow endpoints
    +API_PATH_PREFIX = "/api/"
    +
    +# Test-only endpoints that should not have CORS blocking
    +TEST_ENDPOINTS = ["/test", "/api/test"]
    +
    +# Localhost addresses
    +LOCALHOST_VARIANTS = ["localhost", "127.0.0.1", "[::1]", "0.0.0.0"]
    +CORS_LOCALHOST_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"]
    +
    +# Private IP range start values for 172.16.0.0/12
    +PRIVATE_172_RANGE_START = 16
    +PRIVATE_172_RANGE_END = 32
    +
    +# Regex patterns for localhost origins
    +LOCALHOST_ORIGIN_PATTERNS = [
    +    r"^http://localhost(:[0-9]+)?$",
    +    r"^http://127\.0\.0\.1(:[0-9]+)?$",
    +    r"^http://\[::1\](:[0-9]+)?$",
    +]
    +
    +
    +def get_localhost_addresses() -> list[str]:
    +    """Get localhost/loopback addresses."""
    +    return LOCALHOST_VARIANTS
    +
    +
    +def get_private_ip_patterns() -> list[str]:
    +    """
    +    Generate wildcard patterns for private IP ranges.
    +
    +    These are the standard RFC-defined private address ranges:
    +    - RFC 1918 (IPv4): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
    +      https://datatracker.ietf.org/doc/html/rfc1918
    +    - RFC 4193 (IPv6): fc00::/7
    +      https://datatracker.ietf.org/doc/html/rfc4193
    +
    +    Additional references:
    +    - IANA IPv4 Special-Purpose Address Registry:
    +      https://www.iana.org/assignments/iana-ipv4-special-registry/
    +    - IANA IPv6 Special-Purpose Address Registry:
    +      https://www.iana.org/assignments/iana-ipv6-special-registry/
    +    """
    +    return [
    +        "192.168.*",
    +        "10.*",
    +        *[f"172.{i}.*" for i in range(PRIVATE_172_RANGE_START, PRIVATE_172_RANGE_END)],
    +        "fc00:*",
    +        "fd00:*",
    +    ]
    +
    +
    +def get_allowed_hosts_from_env() -> list[str] | None:
    +    """Get allowed hosts from environment variable."""
    +    if allowed_hosts_env := MLFLOW_SERVER_ALLOWED_HOSTS.get():
    +        return [host.strip() for host in allowed_hosts_env.split(",")]
    +    return None
    +
    +
    +def get_allowed_origins_from_env() -> list[str] | None:
    +    """Get allowed CORS origins from environment variable."""
    +    if allowed_origins_env := MLFLOW_SERVER_CORS_ALLOWED_ORIGINS.get():
    +        return [origin.strip() for origin in allowed_origins_env.split(",")]
    +    return None
    +
    +
    +def is_localhost_origin(origin: str) -> bool:
    +    """Check if an origin is from localhost."""
    +    if not origin:
    +        return False
    +
    +    try:
    +        parsed = urlparse(origin)
    +        hostname = parsed.hostname
    +        return hostname in CORS_LOCALHOST_HOSTS
    +    except Exception:
    +        return False
    +
    +
    +def should_block_cors_request(origin: str, method: str, allowed_origins: list[str] | None) -> bool:
    +    """Determine if a CORS request should be blocked."""
    +    if not origin or method not in STATE_CHANGING_METHODS:
    +        return False
    +
    +    if is_localhost_origin(origin):
    +        return False
    +
    +    if allowed_origins:
    +        # If wildcard "*" is in the list, allow all origins
    +        if "*" in allowed_origins:
    +            return False
    +        if origin in allowed_origins:
    +            return False
    +
    +    return True
    +
    +
    +def is_api_endpoint(path: str) -> bool:
    +    """Check if a path is an API endpoint that should have CORS/OPTIONS handling."""
    +    return path.startswith(API_PATH_PREFIX) and path not in TEST_ENDPOINTS
    +
    +
    +def is_allowed_host_header(allowed_hosts: list[str], host: str) -> bool:
    +    """Validate if the host header matches allowed patterns."""
    +    if not host:
    +        return False
    +
    +    # If wildcard "*" is in the list, allow all hosts
    +    if "*" in allowed_hosts:
    +        return True
    +
    +    return any(
    +        fnmatch.fnmatch(host, allowed) if "*" in allowed else host == allowed
    +        for allowed in allowed_hosts
    +    )
    +
    +
    +def get_default_allowed_hosts() -> list[str]:
    +    """Get default allowed hosts patterns."""
    +    wildcard_hosts = []
    +    for host in get_localhost_addresses():
    +        if host.startswith("["):
    +            # IPv6: escape opening bracket for fnmatch
    +            escaped = host.replace("[", "[[]", 1)
    +            wildcard_hosts.append(f"{escaped}:*")
    +        else:
    +            wildcard_hosts.append(f"{host}:*")
    +
    +    return get_localhost_addresses() + wildcard_hosts + get_private_ip_patterns()
    
  • mlflow/utils/cli_args.py+55 3 modified
    @@ -163,9 +163,11 @@ def _create_env_manager_option(help_string, default=None):
         envvar="MLFLOW_HOST",
         metavar="HOST",
         default="127.0.0.1",
    -    help="The network address to listen on (default: 127.0.0.1). "
    -    "Use 0.0.0.0 to bind to all addresses if you want to access the tracking "
    -    "server from other machines.",
    +    help="The network interface to bind the server to (default: 127.0.0.1). "
    +    "This controls which network interfaces accept connections. "
    +    "Use '127.0.0.1' for local-only access, or '0.0.0.0' to allow connections from any network. "
    +    "NOTE: This is NOT a security setting - it only controls network binding. "
    +    "To restrict which clients can connect, use --allowed-hosts.",
     )
     
     PORT = click.option(
    @@ -256,3 +258,53 @@ def _create_env_manager_option(help_string, default=None):
         "Note: This option only works with the UBUNTU base image; "
         "Python base images do not support Java installation.",
     )
    +
    +# Security-related options for MLflow server
    +ALLOWED_HOSTS = click.option(
    +    "--allowed-hosts",
    +    envvar="MLFLOW_SERVER_ALLOWED_HOSTS",
    +    default=None,
    +    help="Comma-separated list of allowed Host headers to prevent DNS rebinding attacks "
    +    "(default: localhost + private IPs). "
    +    "DNS rebinding allows attackers to trick your browser into accessing internal services. "
    +    "Examples: 'mlflow.company.com,10.0.0.100:5000'. "
    +    "Supports wildcards: 'mlflow.company.com,192.168.*,app-*.internal.com'. "
    +    "Use '*' to allow ALL hosts (not recommended for production). "
    +    "Default allows: localhost (all ports), private IPs (10.*, 192.168.*, 172.16-31.*). "
    +    "Set this when exposing MLflow beyond localhost to prevent host header attacks.",
    +)
    +
    +CORS_ALLOWED_ORIGINS = click.option(
    +    "--cors-allowed-origins",
    +    envvar="MLFLOW_SERVER_CORS_ALLOWED_ORIGINS",
    +    default=None,
    +    help="Comma-separated list of allowed CORS origins to prevent cross-site request attacks "
    +    "(default: localhost origins on any port). "
    +    "CORS attacks allow malicious websites to make requests to your MLflow server using your "
    +    "credentials. Examples: 'https://app.company.com,https://notebook.company.com'. "
    +    "Default allows: http://localhost:* (any port), http://127.0.0.1:*, http://[::1]:*. "
    +    "Set this when you have web applications on different domains that need to access MLflow. "
    +    "Use '*' to allow ALL origins (DANGEROUS - only for development!).",
    +)
    +
    +DISABLE_SECURITY_MIDDLEWARE = click.option(
    +    "--disable-security-middleware",
    +    envvar="MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE",
    +    is_flag=True,
    +    default=False,
    +    help="DANGEROUS: Disable all security middleware including CORS protection and host "
    +    "validation. This completely removes security protections and should only be used for "
    +    "testing. When disabled, your MLflow server is vulnerable to CORS attacks, DNS rebinding, "
    +    "and clickjacking. Instead, prefer configuring specific security settings with "
    +    "--cors-allowed-origins and --allowed-hosts.",
    +)
    +
    +X_FRAME_OPTIONS = click.option(
    +    "--x-frame-options",
    +    envvar="MLFLOW_SERVER_X_FRAME_OPTIONS",
    +    default="SAMEORIGIN",
    +    help="X-Frame-Options header value for clickjacking protection. "
    +    "Options: 'SAMEORIGIN' (default - allows embedding only from same origin), "
    +    "'DENY' (prevents all embedding), 'NONE' (disables header - allows embedding from anywhere). "
    +    "Set to 'NONE' if you need to embed MLflow UI in iframes from different origins.",
    +)
    
  • pyproject.release.toml+1 0 modified
    @@ -29,6 +29,7 @@ requires-python = ">=3.10"
     dependencies = [
       "mlflow-skinny==3.4.1.dev0",
       "mlflow-tracing==3.4.1.dev0",
    +  "Flask-CORS<7",
       "Flask<4",
       "alembic<2,!=1.10.0",
       "cryptography<46,>=43.0.0",
    
  • pyproject.toml+1 0 modified
    @@ -27,6 +27,7 @@ classifiers = [
     ]
     requires-python = ">=3.10"
     dependencies = [
    +  "Flask-CORS<7",
       "Flask<4",
       "alembic<2,!=1.10.0",
       "cachetools<7,>=5.0.0",
    
  • requirements/core-requirements.yaml+4 0 modified
    @@ -19,6 +19,10 @@ flask:
       pip_release: Flask
       max_major_version: 3
     
    +flask-cors:
    +  pip_release: Flask-CORS
    +  max_major_version: 6
    +
     numpy:
       pip_release: numpy
       max_major_version: 2
    
  • tests/server/conftest.py+43 0 added
    @@ -0,0 +1,43 @@
    +import pytest
    +from flask import Flask
    +from werkzeug.test import Client
    +
    +
    +@pytest.fixture
    +def test_app():
    +    """Minimal Flask app for unit testing."""
    +    app = Flask(__name__)
    +
    +    @app.route("/test")
    +    def test_endpoint():
    +        return "OK"
    +
    +    @app.route("/api/test", methods=["GET", "POST", "OPTIONS"])
    +    def api_endpoint():
    +        return "OK"
    +
    +    @app.route("/health")
    +    def health():
    +        return "OK"
    +
    +    @app.route("/version")
    +    def version():
    +        return "OK"
    +
    +    return app
    +
    +
    +@pytest.fixture
    +def mlflow_app_client():
    +    """Test client for the MLflow Flask application with security middleware."""
    +    from flask import Flask
    +
    +    from mlflow.server import handlers, security
    +
    +    # Create a fresh app for each test to avoid state pollution
    +    app = Flask(__name__)
    +    for http_path, handler, methods in handlers.get_endpoints():
    +        app.add_url_rule(http_path, handler.__name__, handler, methods=methods)
    +
    +    security.init_security_middleware(app)
    +    return Client(app)
    
  • tests/server/test_security_integration.py+123 0 added
    @@ -0,0 +1,123 @@
    +import json
    +from unittest import mock
    +
    +import pytest
    +from werkzeug.test import Client
    +
    +
    +@pytest.mark.parametrize(
    +    ("host", "origin", "expected_status", "should_block"),
    +    [
    +        ("evil.attacker.com:5000", "http://evil.attacker.com:5000", 403, True),
    +        ("localhost:5000", None, None, False),
    +    ],
    +)
    +def test_dns_rebinding_and_cors_protection(
    +    mlflow_app_client, host, origin, expected_status, should_block
    +):
    +    headers = {"Host": host, "Content-Type": "application/json"}
    +    if origin:
    +        headers["Origin"] = origin
    +
    +    response = mlflow_app_client.post(
    +        "/api/2.0/mlflow/experiments/search",
    +        headers=headers,
    +        data=json.dumps({"order_by": ["creation_time DESC", "name ASC"], "max_results": 50}),
    +    )
    +
    +    if should_block:
    +        assert response.status_code == expected_status
    +        assert (
    +            b"Invalid Host header" in response.data
    +            or b"Cross-origin request blocked" in response.data
    +        )
    +    else:
    +        assert response.status_code != 403
    +
    +
    +@pytest.mark.parametrize(
    +    ("origin", "endpoint", "expected_blocked"),
    +    [
    +        ("http://malicious-site.com", "/api/2.0/mlflow/experiments/create", True),
    +        ("http://localhost:3000", "/api/2.0/mlflow/experiments/search", False),
    +    ],
    +)
    +def test_cors_for_state_changing_requests(mlflow_app_client, origin, endpoint, expected_blocked):
    +    response = mlflow_app_client.post(
    +        endpoint,
    +        headers={"Origin": origin, "Content-Type": "application/json"},
    +        data=json.dumps({"name": "test-experiment"} if "create" in endpoint else {}),
    +    )
    +
    +    if expected_blocked:
    +        assert response.status_code == 403
    +        assert b"Cross-origin request blocked" in response.data
    +    else:
    +        assert response.status_code != 403
    +
    +
    +@mock.patch.dict("os.environ", {"MLFLOW_SERVER_CORS_ALLOWED_ORIGINS": "https://trusted-app.com"})
    +def test_cors_with_configured_origins():
    +    from flask import Flask
    +
    +    from mlflow.server import handlers, security
    +
    +    app = Flask(__name__)
    +    for http_path, handler, methods in handlers.get_endpoints():
    +        app.add_url_rule(http_path, handler.__name__, handler, methods=methods)
    +
    +    security.init_security_middleware(app)
    +    client = Client(app)
    +
    +    test_cases = [
    +        ("https://trusted-app.com", False),
    +        ("http://evil.com", True),
    +    ]
    +
    +    for origin, should_block in test_cases:
    +        response = client.post(
    +            "/api/2.0/mlflow/experiments/search",
    +            headers={"Origin": origin, "Content-Type": "application/json"},
    +            data=json.dumps({}),
    +        )
    +
    +        if should_block:
    +            assert response.status_code == 403
    +        else:
    +            assert response.status_code != 403
    +
    +
    +def test_security_headers_on_responses(mlflow_app_client):
    +    response = mlflow_app_client.get("/health")
    +    assert response.headers.get("X-Content-Type-Options") == "nosniff"
    +    assert response.headers.get("X-Frame-Options") == "SAMEORIGIN"
    +
    +
    +@pytest.mark.parametrize(
    +    ("origin", "expected_status", "should_have_cors"),
    +    [
    +        ("http://localhost:3000", 204, True),
    +        ("http://evil.com", None, False),
    +    ],
    +)
    +def test_preflight_options_requests(mlflow_app_client, origin, expected_status, should_have_cors):
    +    response = mlflow_app_client.options(
    +        "/api/2.0/mlflow/experiments/search",
    +        headers={
    +            "Origin": origin,
    +            "Access-Control-Request-Method": "POST",
    +            "Access-Control-Request-Headers": "Content-Type",
    +        },
    +    )
    +
    +    if expected_status:
    +        assert response.status_code == expected_status
    +
    +    if should_have_cors:
    +        assert response.headers.get("Access-Control-Allow-Origin") == origin
    +        assert "POST" in response.headers.get("Access-Control-Allow-Methods", "")
    +    else:
    +        assert (
    +            "Access-Control-Allow-Origin" not in response.headers
    +            or response.headers.get("Access-Control-Allow-Origin") != origin
    +        )
    
  • tests/server/test_security.py+228 0 added
    @@ -0,0 +1,228 @@
    +import os
    +from unittest import mock
    +
    +import pytest
    +from flask import Flask
    +from werkzeug.test import Client
    +
    +from mlflow.server import security
    +from mlflow.server.security_utils import is_allowed_host_header
    +
    +
    +def test_default_allowed_hosts():
    +    hosts = security.get_allowed_hosts()
    +    assert "localhost" in hosts
    +    assert "127.0.0.1" in hosts
    +    assert "[::1]" in hosts
    +    assert "localhost:*" in hosts
    +    assert "127.0.0.1:*" in hosts
    +    assert "[[]::1]:*" in hosts
    +    assert "192.168.*" in hosts
    +    assert "10.*" in hosts
    +
    +
    +def test_custom_allowed_hosts():
    +    with mock.patch.dict(
    +        os.environ, {"MLFLOW_SERVER_ALLOWED_HOSTS": "example.com,app.example.com"}
    +    ):
    +        hosts = security.get_allowed_hosts()
    +        assert "example.com" in hosts
    +        assert "app.example.com" in hosts
    +
    +
    +@pytest.mark.parametrize(
    +    ("host_header", "expected_status", "expected_error"),
    +    [
    +        ("localhost", 200, None),
    +        ("127.0.0.1", 200, None),
    +        ("evil.attacker.com", 403, b"Invalid Host header"),
    +    ],
    +)
    +def test_dns_rebinding_protection(test_app, host_header, expected_status, expected_error):
    +    with mock.patch.dict(os.environ, {"MLFLOW_SERVER_ALLOWED_HOSTS": "localhost,127.0.0.1"}):
    +        security.init_security_middleware(test_app)
    +        client = Client(test_app)
    +
    +        response = client.get("/test", headers={"Host": host_header})
    +        assert response.status_code == expected_status
    +        if expected_error:
    +            assert expected_error in response.data
    +
    +
    +@pytest.mark.parametrize(
    +    ("method", "origin", "expected_cors_header"),
    +    [
    +        ("POST", "http://localhost:3000", "http://localhost:3000"),
    +        ("POST", "http://evil.com", None),
    +        ("POST", None, None),
    +        ("GET", "http://evil.com", None),
    +    ],
    +)
    +def test_cors_protection(test_app, method, origin, expected_cors_header):
    +    with mock.patch.dict(
    +        os.environ,
    +        {"MLFLOW_SERVER_CORS_ALLOWED_ORIGINS": "http://localhost:3000,https://app.example.com"},
    +    ):
    +        security.init_security_middleware(test_app)
    +        client = Client(test_app)
    +
    +        headers = {"Origin": origin} if origin else {}
    +        response = getattr(client, method.lower())("/api/test", headers=headers)
    +        assert response.status_code == 200
    +
    +        if expected_cors_header:
    +            assert response.headers.get("Access-Control-Allow-Origin") == expected_cors_header
    +
    +
    +def test_insecure_cors_mode(test_app):
    +    with mock.patch.dict(os.environ, {"MLFLOW_SERVER_CORS_ALLOWED_ORIGINS": "*"}):
    +        security.init_security_middleware(test_app)
    +        client = Client(test_app)
    +
    +        response = client.post("/api/test", headers={"Origin": "http://evil.com"})
    +        assert response.status_code == 200
    +        assert response.headers.get("Access-Control-Allow-Origin") == "http://evil.com"
    +
    +
    +@pytest.mark.parametrize(
    +    ("origin", "expected_cors_header"),
    +    [
    +        ("http://localhost:3000", "http://localhost:3000"),
    +        ("http://evil.com", None),
    +    ],
    +)
    +def test_preflight_options_request(test_app, origin, expected_cors_header):
    +    with mock.patch.dict(
    +        os.environ, {"MLFLOW_SERVER_CORS_ALLOWED_ORIGINS": "http://localhost:3000"}
    +    ):
    +        security.init_security_middleware(test_app)
    +        client = Client(test_app)
    +
    +        response = client.options(
    +            "/api/test",
    +            headers={
    +                "Origin": origin,
    +                "Access-Control-Request-Method": "POST",
    +                "Access-Control-Request-Headers": "Content-Type",
    +            },
    +        )
    +        assert response.status_code == 200
    +
    +        if expected_cors_header:
    +            assert response.headers.get("Access-Control-Allow-Origin") == expected_cors_header
    +
    +
    +def test_security_headers(test_app):
    +    security.init_security_middleware(test_app)
    +    client = Client(test_app)
    +
    +    response = client.get("/test")
    +    assert response.headers.get("X-Content-Type-Options") == "nosniff"
    +    assert response.headers.get("X-Frame-Options") == "SAMEORIGIN"
    +
    +
    +def test_disable_security_middleware(test_app):
    +    with mock.patch.dict(os.environ, {"MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE": "true"}):
    +        security.init_security_middleware(test_app)
    +        client = Client(test_app)
    +
    +        response = client.get("/test")
    +        assert "X-Content-Type-Options" not in response.headers
    +        assert "X-Frame-Options" not in response.headers
    +
    +        response = client.get("/test", headers={"Host": "evil.com"})
    +        assert response.status_code == 200
    +
    +
    +def test_x_frame_options_configuration():
    +    app = Flask(__name__)
    +
    +    @app.route("/test")
    +    def test():
    +        return "OK"
    +
    +    with mock.patch.dict(os.environ, {"MLFLOW_SERVER_X_FRAME_OPTIONS": "DENY"}):
    +        security.init_security_middleware(app)
    +        client = Client(app)
    +        response = client.get("/test")
    +        assert response.headers.get("X-Frame-Options") == "DENY"
    +
    +    app2 = Flask(__name__)
    +
    +    @app2.route("/test")
    +    def test2():
    +        return "OK"
    +
    +    with mock.patch.dict(os.environ, {"MLFLOW_SERVER_X_FRAME_OPTIONS": "NONE"}):
    +        security.init_security_middleware(app2)
    +        client = Client(app2)
    +        response = client.get("/test")
    +        assert "X-Frame-Options" not in response.headers
    +
    +
    +def test_wildcard_hosts(test_app):
    +    """Test that wildcard hosts allow all."""
    +    with mock.patch.dict(os.environ, {"MLFLOW_SERVER_ALLOWED_HOSTS": "*"}):
    +        security.init_security_middleware(test_app)
    +        client = Client(test_app)
    +
    +        response = client.get("/test", headers={"Host": "any.domain.com"})
    +        assert response.status_code == 200
    +
    +
    +@pytest.mark.parametrize(
    +    ("endpoint", "host_header", "expected_status"),
    +    [
    +        ("/health", "evil.com", 200),
    +        ("/test", "evil.com", 403),
    +    ],
    +)
    +def test_endpoint_security_bypass(test_app, endpoint, host_header, expected_status):
    +    with mock.patch.dict(os.environ, {"MLFLOW_SERVER_ALLOWED_HOSTS": "localhost"}):
    +        security.init_security_middleware(test_app)
    +        client = Client(test_app)
    +
    +        response = client.get(endpoint, headers={"Host": host_header})
    +        assert response.status_code == expected_status
    +
    +
    +@pytest.mark.parametrize(
    +    ("hostname", "expected_valid"),
    +    [
    +        ("192.168.1.1", True),
    +        ("10.0.0.1", True),
    +        ("172.16.0.1", True),
    +        ("127.0.0.1", True),
    +        ("localhost", True),
    +        ("[::1]", True),
    +        ("192.168.1.1:8080", True),
    +        ("[::1]:8080", True),
    +        ("evil.com", False),
    +    ],
    +)
    +def test_host_validation(hostname, expected_valid):
    +    hosts = security.get_allowed_hosts()
    +    assert is_allowed_host_header(hosts, hostname) == expected_valid
    +
    +
    +@pytest.mark.parametrize(
    +    ("env_var", "env_value", "expected_result"),
    +    [
    +        (
    +            "MLFLOW_SERVER_CORS_ALLOWED_ORIGINS",
    +            "http://app1.com,http://app2.com",
    +            ["http://app1.com", "http://app2.com"],
    +        ),
    +        ("MLFLOW_SERVER_ALLOWED_HOSTS", "app1.com,app2.com:8080", ["app1.com", "app2.com:8080"]),
    +    ],
    +)
    +def test_environment_variable_configuration(env_var, env_value, expected_result):
    +    with mock.patch.dict(os.environ, {env_var: env_value}):
    +        if "ORIGINS" in env_var:
    +            result = security.get_allowed_origins()
    +            for expected in expected_result:
    +                assert expected in result
    +        else:
    +            result = security.get_allowed_hosts()
    +            for expected in expected_result:
    +                assert expected in result
    
  • uv.lock+16 1 modified
    @@ -828,7 +828,7 @@ name = "cffi"
     version = "2.0.0"
     source = { registry = "https://pypi.org/simple" }
     dependencies = [
    -    { name = "pycparser", marker = "implementation_name != 'PyPy'" },
    +    { name = "pycparser", marker = "(python_full_version < '3.13' and implementation_name != 'PyPy') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')" },
     ]
     sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
     wheels = [
    @@ -1650,6 +1650,19 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
     ]
     
    +[[package]]
    +name = "flask-cors"
    +version = "6.0.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "flask" },
    +    { name = "werkzeug" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463, upload-time = "2025-06-11T01:32:08.518Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244, upload-time = "2025-06-11T01:32:07.352Z" },
    +]
    +
     [[package]]
     name = "flask-wtf"
     version = "1.2.2"
    @@ -3262,6 +3275,7 @@ dependencies = [
         { name = "fastapi" },
         { name = "fastmcp" },
         { name = "flask" },
    +    { name = "flask-cors" },
         { name = "gitpython" },
         { name = "graphene" },
         { name = "gunicorn", marker = "sys_platform != 'win32'" },
    @@ -3428,6 +3442,7 @@ requires-dist = [
         { name = "fastapi", marker = "extra == 'genai'", specifier = "<1" },
         { name = "fastmcp", specifier = ">=2.0.0,<3" },
         { name = "flask", specifier = "<4" },
    +    { name = "flask-cors", specifier = "<7" },
         { name = "flask-wtf", marker = "extra == 'auth'", specifier = "<2" },
         { name = "gitpython", specifier = ">=3.1.9,<4" },
         { name = "google-cloud-storage", marker = "extra == 'databricks'", specifier = ">=1.30.0" },
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.