CVE-2024-2383 (Python, published 2024-06-06T19:15Z)
Description: value: A clickjacking vulnerability exists in zenml-io/zenml versions up to and including 0.55.5 due to the application's failure to set appropriate X-Frame-Options or Content-Security-Policy HTTP headers. This vulnerability allows an attacker to embed the application UI within an iframe on a malicious page, potentially leading to unauthorized actions by tricking users into interacting with the interface under the attacker's control. The issue was addressed in version 0.56.3.
Severity: nan (CVSS v3 base score: )
Weakness: CWE-1021 \u2014 Improper Restriction of Rendered UI Layers or Frames
The web application does not restrict or incorrectly restricts frame objects or UI layers that belong to another application or domain, which can lead to user confusion about which interface the user is interacting with.
Repository: https://github.com/zenml-io/zenml
Files changed:
docker/base.Dockerfile
docker/zenml-server-dev.Dockerfile
docker/zenml-server-hf-spaces.Dockerfile
docs/book/deploying-zenml/zenml-self-hosted/deploy-with-docker.md
pyproject.toml
src/zenml/config/server_config.py
src/zenml/constants.py
src/zenml/zen_server/deploy/docker/docker_zen_server.py
src/zenml/zen_server/deploy/helm/templates/_environment.tpl
src/zenml/zen_server/deploy/helm/values.yaml
src/zenml/zen_server/deploy/local/local_zen_server.py
src/zenml/zen_server/utils.py
src/zenml/zen_server/zen_server_api.py
Commit message: Add security headers to the ZenML server (#2583)

* Add security headers to the ZenML server

* Make secure servers configurable and add sane default values

* Add secure headers options to helm chart and documentation

* Disable uvicorn's server headers

---------

Co-authored-by: Alex Strick van Linschoten <strickvl@users.noreply.github.com>

Vulnerable code:
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app",  "--log-level", "debug"]
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app",  "--log-level", "debug"]
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app",  "--log-level", "debug"]
from typing import Any, Dict, List, Optional

Fixed code:
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"]
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"]
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"]
The environment variables starting with **ZENML\_SERVER\_SECURE*\_HEADERS\_** can be used to enable, disable or set custom values for security headers in the ZenML server's HTTP responses. The following values can be set for any of the supported secure headers configuration options:

* `enabled`, `on`, `true` or `yes` - enables the secure header with the default value.
* `disabled`, `off`, `false`, `none` or `no` - disables the secure header entirely, so that it is not set in the ZenML server's HTTP responses.
* any other value - sets the secure header to the specified value.  

The following secure headers environment variables are supported:

* **ZENML\_SERVER\_SECURE*\_HEADERS\_SERVER**: The `Server` HTTP header value used to identify the server. The default value is the ZenML server ID.
* **ZENML\_SERVER\_SECURE\_HEADERS\_HSTS**: The `Strict-Transport-Security` HTTP header value. The default value is `max-age=63072000; includeSubDomains`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_XFO**: The `X-Frame-Options` HTTP header value. The default value is `SAMEORIGIN`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_XXP**: The `X-XSS-Protection` HTTP header value. The default value is `0`. NOTE: this header is deprecated and should not be customized anymore. The `Content-Security-Policy` header should be used instead.
* **ZENML\_SERVER\_SECURE\_HEADERS\_CONTENT**: The `X-Content-Type-Options` HTTP header value. The default value is `nosniff`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_CSP**: The `Content-Security-Policy` HTTP header value. This is by default set to a strict CSP policy that only allows content from the origins required by the ZenML dashboard. NOTE: customizing this header is discouraged, as it may cause the ZenML dashboard to malfunction.
* **ZENML\_SERVER\_SECURE\_HEADERS\_REFERRER**: The `Referrer-Policy` HTTP header value. The default value is `no-referrer-when-downgrade`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_CACHE**: The `Cache-Control` HTTP header value. The default value is `no-store, no-cache, must-revalidate`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_PERMISSIONS**: The `Permissions-Policy` HTTP header value. The default value is `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()`.

secure = { version = "~0.3.0", optional = true }
    "secure",
from typing import Any, Dict, List, Optional, Union
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_CACHE,
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_CONTENT,
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_CSP,
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_HSTS,
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_PERMISSIONS,
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_REFERRER,
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_XFO,
    DEFAULT_ZENML_SERVER_SECURE_HEADERS_XXP,
        secure_headers_server: Custom value to be set in the `Server` HTTP
            header to identify the server. If not specified, or if set to one of
            the reserved values `enabled`, `yes`, `true`, `on`, the `Server`
            header will be set to the default value (ZenML server ID). If set to
            one of the reserved values `disabled`, `no`, `none`, `false`, `off`
            or to an empty string, the `Server` header will not be included in
            responses.
        secure_headers_hsts: The server header value to be set in the HTTP
            header `Strict-Transport-Security`. If not specified, or if set to
            one of the reserved values `enabled`, `yes`, `true`, `on`, the
            `Strict-Transport-Security` header will be set to the default value
            (`max-age=63072000; includeSubdomains`). If set to one of
            the reserved values `disabled`, `no`, `none`, `false`, `off` or to
            an empty string, the `Strict-Transport-Security` header will not be
            included in responses.
        secure_headers_xfo: The server header value to be set in the HTTP
            header `X-Frame-Options`. If not specified, or if set to one of the
            reserved values `enabled`, `yes`, `true`, `on`, the `X-Frame-Options`
            header will be set to the default value (`SAMEORIGIN`). If set to
            one of the reserved values `disabled`, `no`, `none`, `false`, `off`
            or to an empty string, the `X-Frame-Options` header will not be
            included in responses.
        secure_headers_xxp: The server header value to be set in the HTTP
            header `X-XSS-Protection`. If not specified, or if set to one of the
            reserved values `enabled`, `yes`, `true`, `on`, the `X-XSS-Protection`
            header will be set to the default value (`0`). If set to one of the
            reserved values `disabled`, `no`, `none`, `false`, `off` or
            to an empty string, the `X-XSS-Protection` header will not be
            included in responses. NOTE: this header is deprecated and should
            always be set to `0`. The `Content-Security-Policy` header should be
            used instead.
        secure_headers_content: The server header value to be set in the HTTP
            header `X-Content-Type-Options`. If not specified, or if set to one
            of the reserved values `enabled`, `yes`, `true`, `on`, the
            `X-Content-Type-Options` header will be set to the default value
            (`nosniff`). If set to one of the reserved values `disabled`, `no`,
            `none`, `false`, `off` or to an empty string, the
            `X-Content-Type-Options` header will not be included in responses.
        secure_headers_csp: The server header value to be set in the HTTP
            header `Content-Security-Policy`. If not specified, or if set to one
            of the reserved values `enabled`, `yes`, `true`, `on`, the
            `Content-Security-Policy` header will be set to a default value
            that is compatible with the ZenML dashboard. If set to one of the
            reserved values `disabled`, `no`, `none`, `false`, `off` or to an
            empty string, the `Content-Security-Policy` header will not be
            included in responses.
        secure_headers_referrer: The server header value to be set in the HTTP
            header `Referrer-Policy`. If not specified, or if set to one of the
            reserved values `enabled`, `yes`, `true`, `on`, the `Referrer-Policy`
            header will be set to the default value
            (`no-referrer-when-downgrade`). If set to one of the reserved values
            `disabled`, `no`, `none`, `false`, `off` or to an empty string, the
            `Referrer-Policy` header will not be included in responses.
        secure_headers_cache: The server header value to be set in the HTTP
            header `Cache-Control`. If not specified, or if set to one of the
            reserved values `enabled`, `yes`, `true`, `on`, the `Cache-Control`
            header will be set to the default value
            (`no-store, no-cache, must-revalidate`). If set to one of the
            reserved values `disabled`, `no`, `none`, `false`, `off` or to an
            empty string, the `Cache-Control` header will not be included in
            responses.
        secure_headers_permissions: The server header value to be set in the
            HTTP header `Permissions-Policy`. If not specified, or if set to one
            of the reserved values `enabled`, `yes`, `true`, `on`, the
            `Permissions-Policy` header will be set to the default value
            (`accelerometer=(), camera=(), geolocation=(), gyroscope=(),
              magnetometer=(), microphone=(), payment=(), usb=()`). If set to
            one of the reserved values `disabled`, `no`, `none`, `false`, `off`
            or to an empty string, the `Permissions-Policy` header will not be
            included in responses.
    secure_headers_server: Union[bool, str] = True
    secure_headers_hsts: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_HSTS
    )
    secure_headers_xfo: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_XFO
    )
    secure_headers_xxp: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_XXP
    )
    secure_headers_content: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_CONTENT
    )
    secure_headers_csp: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_CSP
    )
    secure_headers_referrer: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_REFERRER
    )
    secure_headers_cache: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_CACHE
    )
    secure_headers_permissions: Union[bool, str] = (
        DEFAULT_ZENML_SERVER_SECURE_HEADERS_PERMISSIONS
    )

        # if one of the secure headers options is set to a boolean value, set
        # the corresponding value
        for k, v in values.copy().items():
            if k.startswith("secure_headers_") and isinstance(v, str):
                if v.lower() in ["disabled", "no", "none", "false", "off", ""]:
                    values[k] = False
                if v.lower() in ["enabled", "yes", "true", "on"]:
                    # Revert to the default value if the header is enabled
                    del values[k]

DEFAULT_ZENML_SERVER_SECURE_HEADERS_HSTS = (
    "max-age=63072000; includeSubdomains"
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XFO = "SAMEORIGIN"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XXP = "0"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CONTENT = "nosniff"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CSP = (
    "default-src 'none'; "
    "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
    "connect-src 'self' https://sdkdocs.zenml.io https://hubapi.zenml.io; "
    "img-src 'self' data: https://public-flavor-logos.s3.eu-central-1.amazonaws.com; "
    "style-src 'self' 'unsafe-inline'; "
    "base-uri 'self'; "
    "form-action 'self'; "
    "font-src 'self';"
    "frame-src https://zenml.hellonext.co https://sdkdocs.zenml.io "
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_REFERRER = "no-referrer-when-downgrade"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CACHE = (
    "no-store, no-cache, must-revalidate"
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_PERMISSIONS = (
    "accelerometer=(), autoplay=(), camera=(), encrypted-media=(), "
    "geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), "
    "payment=(), sync-xhr=(), usb=()"
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_REPORT_TO = "default"

                server_header=False,
{{- range $key, $value := .ZenML.secure_headers }}
secure_headers_{{ $key }}: {{ $value | quote }}
{{- end }}
  # The ZenML server's secure headers configuration. This can be used to
  # enable, disable or set custom values for security headers in the ZenML
  # server's HTTP responses. The following values can be set for any of the
  # supported secure headers configuration options:
  #
  # - `enabled`, `on`, `true` or `yes` - enables the secure header with the
  # default value.
  # - `disabled`, `off`, `false`, `none` or `no` - disables the secure header
  # entirely, so that it is not set in the ZenML server's HTTP responses.
  # - any other value - sets the secure header to the specified value.  
  secure_headers:
    # The `Server` HTTP header value used to identify the server. The default
    # value is the ZenML server ID.
    server: enabled
    # The `Strict-Transport-Security` HTTP header value. The default value is
    # `max-age=63072000; includeSubDomains`.
    hsts: enabled
    # The `X-Frame-Options` HTTP header value. The default value is `SAMEORIGIN`.
    xfo: enabled
    # The `X-XSS-Protection` HTTP header value. The default value is `0`.
    # NOTE: this header is deprecated and should not be customized anymore. The
    # `Content-Security-Policy` header should be used instead.
    xxp: enabled
    # The `X-Content-Type-Options` HTTP header value. The default value is
    # `nosniff`.
    content: enabled
    # The `Content-Security-Policy` HTTP header value. This is by default set
    # to a strict CSP policy that only allows content from the origins required
    # by the ZenML dashboard.
    # NOTE: customizing this header is discouraged, as it may cause the ZenML
    # dashboard to malfunction.
    csp: enabled
    # The `Referrer-Policy` HTTP header value. The default value is
    # `no-referrer-when-downgrade`.
    referrer: enabled
    # The `Cache-Control` HTTP header value. The default value is
    # `no-store, no-cache, must-revalidate`.
    cache: enabled
    # The `Permissions-Policy` HTTP header value. The default value is
    # `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()`.
    permissions: enabled


                server_header=False,
import secure
_secure_headers: Optional[secure.Secure] = None
def secure_headers() -> secure.Secure:
    """Return the secure headers component.

    Returns:
        The secure headers component.

    Raises:
        RuntimeError: If the secure headers component is not initialized.
    """
    global _secure_headers
    if _secure_headers is None:
        raise RuntimeError("Secure headers component not initialized")
    return _secure_headers


def initialize_secure_headers() -> None:
    """Initialize the secure headers component."""
    global _secure_headers

    config = server_config()

    # For each of the secure headers supported by the `secure` library, we
    # check if the corresponding configuration is set in the server
    # configuration:
    #
    # - if set to `True`, we use the default value for the header
    # - if set to a string, we use the string as the value for the header
    # - if set to `False`, we don't set the header

    server: Optional[secure.Server] = None
    if config.secure_headers_server:
        server = secure.Server()
        if isinstance(config.secure_headers_server, str):
            server.set(config.secure_headers_server)
        else:
            server.set(str(config.deployment_id))

    hsts: Optional[secure.StrictTransportSecurity] = None
    if config.secure_headers_hsts:
        hsts = secure.StrictTransportSecurity()
        if isinstance(config.secure_headers_hsts, str):
            hsts.set(config.secure_headers_hsts)

    xfo: Optional[secure.XFrameOptions] = None
    if config.secure_headers_xfo:
        xfo = secure.XFrameOptions()
        if isinstance(config.secure_headers_xfo, str):
            xfo.set(config.secure_headers_xfo)

    xxp: Optional[secure.XXSSProtection] = None
    if config.secure_headers_xxp:
        xxp = secure.XXSSProtection()
        if isinstance(config.secure_headers_xxp, str):
            xxp.set(config.secure_headers_xxp)

    csp: Optional[secure.ContentSecurityPolicy] = None
    if config.secure_headers_csp:
        csp = secure.ContentSecurityPolicy()
        if isinstance(config.secure_headers_csp, str):
            csp.set(config.secure_headers_csp)

    content: Optional[secure.XContentTypeOptions] = None
    if config.secure_headers_content:
        content = secure.XContentTypeOptions()
        if isinstance(config.secure_headers_content, str):
            content.set(config.secure_headers_content)

    referrer: Optional[secure.ReferrerPolicy] = None
    if config.secure_headers_referrer:
        referrer = secure.ReferrerPolicy()
        if isinstance(config.secure_headers_referrer, str):
            referrer.set(config.secure_headers_referrer)

    cache: Optional[secure.CacheControl] = None
    if config.secure_headers_cache:
        cache = secure.CacheControl()
        if isinstance(config.secure_headers_cache, str):
            cache.set(config.secure_headers_cache)

    permissions: Optional[secure.PermissionsPolicy] = None
    if config.secure_headers_permissions:
        permissions = secure.PermissionsPolicy()
        if isinstance(config.secure_headers_permissions, str):
            permissions.value = config.secure_headers_permissions

    _secure_headers = secure.Secure(
        server=server,
        hsts=hsts,
        xfo=xfo,
        xxp=xxp,
        csp=csp,
        content=content,
        referrer=referrer,
        cache=cache,
        permissions=permissions,
    )


    initialize_secure_headers,
    secure_headers,
@app.middleware("http")
async def set_secure_headers(request: Request, call_next: Any) -> Any:
    """Middleware to set secure headers.

    Args:
        request: The incoming request.
        call_next: The next function to be called.

    Returns:
        The response with secure headers set.
    """
    response = await call_next(request)
    secure_headers().framework.fastapi(response)
    return response


    initialize_secure_headers()