Moderate severityNVD Advisory· Published Jun 6, 2024· Updated Aug 1, 2024
Clickjacking Vulnerability in zenml-io/zenml
CVE-2024-2383
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
zenmlPyPI | < 0.56.3 | 0.56.3 |
Affected products
1- Range: unspecified
Patches
1f863fde1269bAdd security headers to the ZenML server (#2583)
13 files changed · +333 −4
docker/base.Dockerfile+1 −1 modified@@ -37,5 +37,5 @@ RUN groupadd --gid $USER_GID $USERNAME \ RUN mkdir -p /zenml/.zenconfig/local_stores/default_zen_store && chown -R $USER_UID:$USER_GID /zenml ENV PATH="$PATH:/home/$USERNAME/.local/bin" -ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug"] +ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"] CMD ["--port", "8080", "--host", "0.0.0.0"]
docker/zenml-server-dev.Dockerfile+1 −1 modified@@ -46,5 +46,5 @@ ENV ZENML_CONFIG_PATH=/zenml/.zenconfig \ ZENML_DEBUG=true \ ZENML_ANALYTICS_OPT_IN=false -ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug"] +ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"] CMD ["--port", "8080", "--host", "0.0.0.0"]
docker/zenml-server-hf-spaces.Dockerfile+1 −1 modified@@ -59,5 +59,5 @@ ENV ZENML_SERVER_DEPLOYMENT_TYPE="hf_spaces" # ENV ZENML_SECRETS_STORE_VAULT_NAMESPACE="" # ENV ZENML_SECRETS_STORE_MAX_VERSIONS="" -ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug"] +ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"] CMD ["--port", "8080", "--host", "0.0.0.0"]
docs/book/deploying-zenml/zenml-self-hosted/deploy-with-docker.md+18 −0 modified@@ -248,6 +248,24 @@ These configuration options are not required for most use cases, but can be usef openssl rand -hex 32 ``` +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=()`. + ## Run the ZenML server with Docker As previously mentioned, the ZenML server container image uses sensible defaults for most configuration options. This means that you can simply run the container with Docker without any additional configuration and it will work out of the box for most use cases:
pyproject.toml+2 −0 modified@@ -73,6 +73,7 @@ fastapi-utils = { version = "~0.2.1", optional = true } orjson = { version = "~3.10.0", optional = true } Jinja2 = { version = "*", optional = true } ipinfo = { version = ">=4.4.3", optional = true } +secure = { version = "~0.3.0", optional = true } # Optional dependencies for project templates copier = { version = ">=8.1.0", optional = true } @@ -181,6 +182,7 @@ server = [ "orjson", "Jinja2", "ipinfo", + "secure", ] templates = ["copier", "jinja2-time", "ruff", "pyyaml-include"] terraform = ["python-terraform"]
src/zenml/config/server_config.py+115 −1 modified@@ -16,7 +16,7 @@ import json import os from secrets import token_hex -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from uuid import UUID from pydantic import BaseModel, Field, SecretStr, root_validator @@ -30,6 +30,14 @@ DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_MINUTE, DEFAULT_ZENML_SERVER_MAX_DEVICE_AUTH_ATTEMPTS, DEFAULT_ZENML_SERVER_PIPELINE_RUN_AUTH_WINDOW, + 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, ENV_ZENML_SERVER_PREFIX, ) from zenml.enums import AuthScheme @@ -128,6 +136,76 @@ class ServerConfiguration(BaseModel): server. login_rate_limit_minute: The number of login attempts allowed per minute. login_rate_limit_day: The number of login attempts allowed per day. + 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. """ deployment_type: ServerDeploymentType = ServerDeploymentType.OTHER @@ -171,6 +249,32 @@ class ServerConfiguration(BaseModel): login_rate_limit_minute: int = DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_MINUTE login_rate_limit_day: int = DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_DAY + 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 + ) + _deployment_id: Optional[UUID] = None @root_validator(pre=True) @@ -221,6 +325,16 @@ def _validate_config(cls, values: Dict[str, Any]) -> Dict[str, Any]: f"The server metadata is not a valid JSON string: {e}" ) + # 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] + return values @property
src/zenml/constants.py+28 −0 modified@@ -236,6 +236,34 @@ def handle_int_env_var(var: str, default: int = 0) -> int: DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_MINUTE = 5 DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_DAY = 1000 +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" + # Configurations to decide which resources report their usage and check for # entitlement in the case of a cloud deployment. Expected Format is this: # ENV_ZENML_REPORTABLE_RESOURCES='["Foo", "bar"]'
src/zenml/zen_server/deploy/docker/docker_zen_server.py+1 −0 modified@@ -205,6 +205,7 @@ def run(self) -> None: host="0.0.0.0", # nosec port=self.endpoint.config.port or 8000, log_level="info", + server_header=False, ) except KeyboardInterrupt: logger.info("ZenML Server stopped. Resuming normal execution.")
src/zenml/zen_server/deploy/helm/templates/_environment.tpl+3 −0 modified@@ -88,6 +88,9 @@ base_url: {{ .ZenML.baseURL | quote }} {{- if .ZenML.auth.rbacImplementationSource }} rbac_implementation_source: {{ .ZenML.auth.rbacImplementationSource | quote }} {{- end }} +{{- range $key, $value := .ZenML.secure_headers }} +secure_headers_{{ $key }}: {{ $value | quote }} +{{- end }} {{- end }}
src/zenml/zen_server/deploy/helm/values.yaml+43 −0 modified@@ -787,6 +787,49 @@ zenml: # variable in the `zenml.secretEnvironment` variable. class_path: my.custom.secrets.store.MyCustomSecretsStore + # 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 + + # Extra environment variables to set in the ZenML server container. environment: {}
src/zenml/zen_server/deploy/local/local_zen_server.py+1 −0 modified@@ -228,6 +228,7 @@ def run(self) -> None: host=self.endpoint.config.ip_address, port=self.endpoint.config.port or 8000, log_level="info", + server_header=False, ) except KeyboardInterrupt: logger.info("ZenML Server stopped. Resuming normal execution.")
src/zenml/zen_server/utils.py+100 −0 modified@@ -27,6 +27,7 @@ ) from urllib.parse import urlparse +import secure from pydantic import BaseModel, ValidationError from zenml.config.global_config import GlobalConfiguration @@ -59,6 +60,7 @@ _feature_gate: Optional[FeatureGateInterface] = None _workload_manager: Optional[WorkloadManagerInterface] = None _plugin_flavor_registry: Optional[PluginFlavorRegistry] = None +_secure_headers: Optional[secure.Secure] = None def zen_store() -> "SqlZenStore": @@ -214,6 +216,104 @@ def initialize_zen_store() -> None: _zen_store = zen_store_ +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, + ) + + _server_config: Optional[ServerConfiguration] = None
src/zenml/zen_server/zen_server_api.py+19 −0 modified@@ -66,8 +66,10 @@ initialize_feature_gate, initialize_plugins, initialize_rbac, + initialize_secure_headers, initialize_workload_manager, initialize_zen_store, + secure_headers, server_config, ) @@ -121,6 +123,22 @@ def validation_exception_handler( ) +@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 + + @app.middleware("http") async def infer_source_context(request: Request, call_next: Any) -> Any: """A middleware to track the source of an event. @@ -163,6 +181,7 @@ def initialize() -> None: initialize_feature_gate() initialize_workload_manager() initialize_plugins() + initialize_secure_headers() app.mount(
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- github.com/advisories/GHSA-mq73-g4qr-fgcqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-2383ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/zenml/PYSEC-2024-194.yamlghsaWEB
- github.com/zenml-io/zenml/commit/f863fde1269bc355951f8cfc826c0244d88ad5e9ghsaWEB
- huntr.com/bounties/22d26f5a-c0ae-4344-aa7d-08ff5ada3963ghsaWEB
News mentions
0No linked articles in our index yet.