CVE-2023-6568 (Python, published 2023-12-07T05:15Z)
Description: value: A reflected Cross-Site Scripting (XSS) vulnerability exists in the mlflow/mlflow repository, specifically within the handling of the Content-Type header in POST requests. An attacker can inject malicious JavaScript code into the Content-Type header, which is then improperly reflected back to the user without adequate sanitization or escaping, leading to arbitrary JavaScript execution in the context of the victim's browser. The vulnerability is present in the mlflow/server/auth/__init__.py file, where the user-supplied Content-Type header is directly injected into a Python formatted string and returned to the user, facilitating the XSS attack.
Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/mlflow/mlflow
Files changed:
docs/source/auth/rest-api.rst
docs/source/rest-api.rst
mlflow/server/auth/__init__.py
mlflow/server/handlers.py
mlflow/server/validation.py
tests/server/test_handlers.py
Commit message: Add Content-Type header validation (#10526)

Signed-off-by: B-Step62 <yuki.watanabe@databricks.com>

Vulnerable code:

        return make_response(f"Invalid content type: '{content_type}'", 400)

Fixed code:
.. important::
    The MLflow REST API requires content type ``application/json`` for all POST requests.
.. important::
    The MLflow REST API requires content type ``application/json`` for all POST requests.

        message = (
            "Invalid content type. Must be one of: "
            "application/x-www-form-urlencoded, application/json"
        )
        return make_response(message, 400)
from mlflow.server.validation import _validate_content_type
    _validate_content_type(flask_request, ["application/json"])
    _validate_content_type(request, ["application/json"])
    _validate_content_type(request, ["application/json"])

from typing import List

from mlflow.exceptions import MlflowException
from mlflow.protos.databricks_pb2 import INVALID_PARAMETER_VALUE


def _validate_content_type(flask_request, allowed_content_types: List[str]):
    """
    Validates that the request content type is one of the allowed content types.

    :param flask_request: Flask request object (flask.request)
    :param allowed_content_types: A list of allowed content types
    """
    if flask_request.method not in ["POST", "PUT"]:
        return

    if flask_request.content_type is None:
        raise MlflowException(
            message="Bad Request. Content-Type header is missing.",
            error_code=INVALID_PARAMETER_VALUE,
        )

    # Remove any parameters e.g. "application/json; charset=utf-8" -> "application/json"
    content_type = flask_request.content_type.split(";")[0]
    if content_type not in allowed_content_types:
        message = f"Bad Request. Content-Type must be one of {allowed_content_types}."

        raise MlflowException(
            message=message,
            error_code=INVALID_PARAMETER_VALUE,
        )
    request.content_type = "application/json"
    request.content_type = "application/json"
def test_can_parse_post_json_with_content_type_params():
    request = mock.MagicMock()
    request.method = "POST"
    request.content_type = "application/json; charset=utf-8"
    request.get_json = mock.MagicMock()
    request.get_json.return_value = {"name": "hello"}
    msg = _get_request_message(CreateExperiment(), flask_request=request)
    assert msg.name == "hello"


    request.content_type = "application/json"
def test_can_block_post_request_with_invalid_content_type():
    request = mock.MagicMock()
    request.method = "POST"
    request.content_type = "text/plain"
    request.get_json = mock.MagicMock()
    request.get_json.return_value = {"name": "hello"}
    with pytest.raises(MlflowException, match=r"Bad Request. Content-Type"):
        _get_request_message(CreateExperiment(), flask_request=request)


def test_can_block_post_request_with_missing_content_type():
    request = mock.MagicMock()
    request.method = "POST"
    request.content_type = None
    request.get_json = mock.MagicMock()
    request.get_json.return_value = {"name": "hello"}
    with pytest.raises(MlflowException, match=r"Bad Request. Content-Type"):
        _get_request_message(CreateExperiment(), flask_request=request)