diff --git a/UPDATING.md b/UPDATING.md index 7cfd118b90b1..0c3e36e1dd94 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -31,6 +31,14 @@ assists people when migrating to a new version. moves to that DB: avoid running old and new versions side by side, and drain in-flight tasks before upgrading. +- [45186](https://github.com/apache/superset/pull/45186): Sets pyarrow>=23.0.1 + in superset-core (without an upper ceiling) and explicitly bounds + msgpack>=1.2.3,<1.3.0 in requirements/base.in. The relaxed floor in core + resolves CVE-2026-25087 while preserving compatibility for downstream + extensions, whereas container images and application builds continue to + enforce the narrower window via requirements/base.in and + requirements/base.txt. + - Refreshing a dataset's columns from its source (the "Sync columns from source" button, `PUT /api/v1/dataset/?override_columns=true`, or `PUT /api/v1/dataset//refresh`) no longer resets **Is dimension** and @@ -177,7 +185,6 @@ assists people when migrating to a new version. time-axis bounds; time-series offsets and separate inner-bound queries replace ranges on their selected axis. Retained predicates can narrow results where earlier releases discarded them. - - **Semantic NULL filters and provider SDK upgrade.** Semantic filters normalize UI NULL/empty-string selections. Mixed positive NULL membership uses `OrFilter`, which every semantic provider must render; previously NULL rows could be diff --git a/docs/admin_docs/security/cves.mdx b/docs/admin_docs/security/cves.mdx index 55b1189566cc..c09cb4aaab47 100644 --- a/docs/admin_docs/security/cves.mdx +++ b/docs/admin_docs/security/cves.mdx @@ -139,3 +139,7 @@ sidebar_position: 2 | CVE-2022-43720 | Improper rendering of user input | < 2.0.1 or < 1.5.2 | | CVE-2022-43721 | Open Redirect Vulnerability | < 2.0.1 or < 1.5.2 | | CVE-2022-45438 | Dashboard metadata information leak | < 2.0.1 or < 1.5.2 | + +--- + +For third-party container scan findings and remediation guidance, see the [Dependency Vulnerability Remediation Guide](./dependency-vulnerability-remediation.mdx). diff --git a/docs/admin_docs/security/dependency-vulnerability-remediation.mdx b/docs/admin_docs/security/dependency-vulnerability-remediation.mdx new file mode 100644 index 000000000000..64a40ad43d58 --- /dev/null +++ b/docs/admin_docs/security/dependency-vulnerability-remediation.mdx @@ -0,0 +1,97 @@ +--- +title: Dependency Vulnerability Remediation & Scanner Guidance +sidebar_position: 3 +--- + +# Dependency Vulnerability Remediation & Container Scanner Guidance + +This guide addresses third-party dependency vulnerabilities reported by container security scanners (such as Trivy, Grype, and Snyk) in Apache Superset environments, specifically regarding dependency constraint handling for cryptography, msgpack, and pyarrow (referencing issue #45087). + +## 1. Vulnerability Applicability & Risk Assessment + +When security scanners evaluate Apache Superset container images, findings frequently arise in transitive or pinned direct dependencies. The table below details the applicability and actual exploitability surface of recently reported findings: + +| Package | Finding ID | Severity | Upstream Fixed Version | Superset Target Floor | Superset Code Path & Exploitability Analysis | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **cryptography** | CVE-2026-69247 | HIGH | `50.0.0` | `>=50.0.2` | Bleichenbacher timing and error decryption oracle in PKCS#7 EnvelopedData. Superset does not consume, parse, or decrypt PKCS#7 or S/MIME enveloped data. | +| **cryptography** | CVE-2026-69249 | HIGH | `49.0.0` | `>=50.0.2` | X.509 path-building algorithm denial of service with duplicate intermediate certificates. Superset parses leaf database SSL certificates using `load_pem_x509_certificate` and does not build untrusted X.509 certification paths during web request processing. | +| **cryptography** | CVE-2026-69248 | MEDIUM | `49.0.0` | `>=50.0.2` | Wildcard-DNS name-constraint bypass in the cryptography X.509 certificate verifier (`cryptography.x509.verification.PolicyBuilder`). Superset parses leaf database SSL certificates using `load_pem_x509_certificate` and does not invoke cryptography's certificate verification or path validation APIs. TLS transport verification is performed by underlying network and database drivers via OpenSSL/Python `ssl`. | +| **cryptography** | GHSA-537c-gmf6-5ccf (CVE-2026-34180) | HIGH | `48.0.1` | `>=50.0.2` | Vulnerabilities in OpenSSL bundled in binary wheels (remediated in upstream wheels from 48.0.1 and pinned to 50.0.2 in Superset locked requirements). In production container deployments following Option A, the runtime libraries are updated; raw cryptographic primitives are not exposed to untrusted external callers. | +| **msgpack** | GHSA-6v7p-g79w-8964 (CVE-2026-57585) | HIGH | `1.2.1` | `>=1.2.3` | Out-of-bounds read and crash when reusing an Unpacker instance after a caught error. Superset uses msgpack exclusively for SQL Lab asynchronous query result serialization (`RESULTS_BACKEND_USE_MSGPACK`); the Unpacker is never reused across error boundaries in production code paths. | +| **msgpack** | GHSA-j586-36cw-2gc2 | HIGH | `1.2.3` | `>=1.2.3` | Memory leak in the C extension when map key validation raises an exception during unpacking (triggers with default `strict_map_key=True` when a map key is not `str`/`bytes`). In Superset, msgpack deserialization via `msgpack.loads(payload, raw=False)` in `superset/views/utils.py` uses this default on cached query results from the results backend, whose provenance is internal Celery worker execution rather than direct user input. Operators should upgrade to `msgpack>=1.2.3` (Option A) to eliminate the leak path directly rather than relying on payload non-reachability. | +| **pyarrow** | CVE-2026-25087 | HIGH | `23.0.1` | `>=23.0.1` (core) / `>=26.0.0` (app) | Use-after-free in the Arrow C++ IPC file reader with pre-buffering (GHSA-rgxp-2hwp-jwgg). Superset invokes the Parquet reader for user data uploads and the IPC stream reader (`RecordBatchStreamReader`) for SQL Lab asynchronous query result serialization; it does not invoke the IPC file reader (`RecordBatchFileReader` with pre-buffering). Upstream fixes start at `23.0.1`, which is enforced as the minimum floor in `superset-core`, while root application requirements pin to `>=26.0.0`. | + +## 2. Dependency Constraint Architecture + +Apache Superset manages dependencies across two layers: + +1. **Package Declarations (`pyproject.toml` and `superset-core/pyproject.toml`)**: + Specifies flexible semantic version ranges to avoid dependency resolver conflicts in downstream custom deployments. + - `cryptography` (`pyproject.toml`): `>=50.0.2, <51.0.0` (enforces remediated releases covering PKCS#7, path-building, certificate verification name constraints, and wheel OpenSSL advisories). + - `msgpack` (`pyproject.toml`): `>=1.2.3, <1.3` (enforces remediated releases covering both GHSA-6v7p-g79w-8964, fixed in 1.2.1, and GHSA-j586-36cw-2gc2, fixed in 1.2.3). + - `pyarrow`: Root `pyproject.toml` declares `>=26.0.0, <27` (pinned to `26.0.0` in `requirements/base.txt`); `superset-core/pyproject.toml` declares `>=23.0.1` (without an upper ceiling) to satisfy CVE-2026-25087 while permitting downstream extensions and application co-installation. + +2. **Locked Requirements (`requirements/base.in` and `requirements/base.txt`)**: + Pins exact validated versions for official Docker images and deterministic continuous integration builds. + +## 3. Operator Mitigations & Container Workarounds + +If you are running an existing container image where dependency constraints prevent direct pip upgrades, the following approaches allow immediate remediation without breaking Superset: + +### Option A: Extend Official Docker Image with Patched Dependencies + +The official Docker image executes Superset inside a Python virtual environment located at `/app/.venv`. To ensure upgraded dependencies are installed directly into Superset runtime rather than the root container system packages, use `uv pip install --python /app/.venv/bin/python` (or `/app/.venv/bin/pip install` in environments without `uv`). + +When upgrading `cryptography>=50.0.2`, `pyOpenSSL` must also be upgraded to `>=26.4.0` (matching the version pinned in `requirements/base.txt`). pyOpenSSL releases prior to 26.4.0 cap `cryptography` below 50 (with 25.x releases also attempting to access `_lib.GEN_EMAIL` when `OpenSSL.crypto` is imported, which is no longer exposed in cryptography 50.x). Upgrading pyOpenSSL to `>=26.4.0` alongside cryptography satisfies the dependency resolver and ensures imports of `OpenSSL` (used by Snowflake private key authentication and Google Cloud mutual TLS) succeed without error: + +```dockerfile +FROM apache/superset:latest + +USER root +RUN uv pip install --no-cache-dir --python /app/.venv/bin/python \ + "cryptography>=50.0.2" \ + "pyopenssl>=26.4.0" \ + "msgpack>=1.2.3" \ + "pyarrow>=26.0.0" + +USER superset +``` + +### Option B: Scanner Triage and Ignore Rules (`.trivyignore`) + +For organizations maintaining automated compliance gates, findings where the specific vulnerable code path is not reachable in the shipped Superset version and configuration can be documented and suppressed in `.trivyignore`. Note that Trivy matches suppression rules against the exact identifier reported by the scan engine; both CVE and GHSA aliases are provided below where applicable so rules match regardless of whether your feeds report under CVE or GitHub Advisory Database identifiers: + +```text +# CVE-2026-69247: PKCS#7 EnvelopedData decryption is not used by Superset +CVE-2026-69247 + +# CVE-2026-69249: Untrusted X.509 certificate path-building is not exposed in request processing +CVE-2026-69249 + +# CVE-2026-69248: Cryptography X.509 certificate verifier APIs (cryptography.x509.verification) are not called by Superset +CVE-2026-69248 + +# Wheel-bundled OpenSSL in cryptography wheels (suppress only when system libssl is patched, or scope to pkg:pypi/cryptography): +CVE-2026-34180 +GHSA-537c-gmf6-5ccf + +# Msgpack Unpacker reuse after error: Unpacker instances are not reused across error boundaries +CVE-2026-57585 +GHSA-6v7p-g79w-8964 + +# CVE-2026-25087 / GHSA-rgxp-2hwp-jwgg: Arrow C++ IPC file reader with pre-buffering is not invoked; Superset calls Parquet for uploads and IPC stream reader for SQL Lab results (remediated in pyarrow>=23.0.1 in core and >=26.0.0 in locked requirements) +CVE-2026-25087 +GHSA-rgxp-2hwp-jwgg +``` + +:::note +`GHSA-j586-36cw-2gc2` is intentionally omitted from the `.trivyignore` template above. Because msgpack deserialization via `msgpack.loads(..., raw=False)` in `superset/views/utils.py` uses the default `strict_map_key=True` which can trigger the C extension memory leak on invalid map keys, operators running `msgpack==1.2.2` should remediate by upgrading to `msgpack>=1.2.3` (Option A) rather than suppressing the finding. +::: + +## 4. Questions & Answers for Security Teams + +**Q: Are these findings considered true vulnerabilities in Apache Superset?** +A: Per the Apache Superset Security Model (documented in `SECURITY.md`), vulnerabilities in shipped direct dependencies are kept in scope, which is why Superset directly bumps dependency floors to remediated releases. For operators managing legacy container images before upgrades are applied, the exploitability analysis in this guide documents the specific execution paths showing why unpatched intermediate environments are not exposed to remote code execution or authentication bypass. + +**Q: Why are dependency bounds not completely unpinned?** +A: Database connectors and analytics engines (e.g., Snowflake, BigQuery, Trino, ClickHouse) impose mutual constraints on foundational packages like PyArrow and Cryptography. Version ranges are carefully calibrated to balance maximum security coverage with multi-database ecosystem stability. diff --git a/requirements/base.in b/requirements/base.in index 5bda3cb57709..beba6de5aa72 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -27,11 +27,16 @@ filelock>=3.20.3,<4.0.0 # Security: decompression bomb fix (required by aiohttp 3.13.3); CVE-2026-111497 brotli>=1.2.1,<2.0.0 numexpr>=2.9.0 -# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation +# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation; +# CVE-2026-69247, CVE-2026-69249, GHSA-537c-gmf6-5ccf, CVE-2026-69248 - certificate verification, path-building, and packaging advisories (#45087) cryptography>=50.0.2,<51.0.0 +# Security: GHSA-6v7p-g79w-8964 / CVE-2026-57585 (HIGH) - Out-of-bounds read/crash on Unpacker reuse; +# GHSA-j586-36cw-2gc2 (HIGH) - C extension memory leak on map key validation failure (#45087) +msgpack>=1.2.3,<1.3.0 # Security: Snyk - XSS vulnerability in Mako templates mako>=1.4.3,<2.0.0 -# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers +# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers; +# CVE-2026-25087 (HIGH) - Use-after-free in IPC file reader with pre-buffering (GHSA-rgxp-2hwp-jwgg) (#45087) pyarrow>=26.0.0,<27.0.0 # Security: CVE-2026-27459 - pyopenssl certificate validation pyopenssl>=26.0.0,<27.0.0 diff --git a/superset-core/pyproject.toml b/superset-core/pyproject.toml index 497172cd0c3f..90db570fcef3 100644 --- a/superset-core/pyproject.toml +++ b/superset-core/pyproject.toml @@ -43,7 +43,7 @@ classifiers = [ dependencies = [ "flask-appbuilder>=5.2.0,<6", "isodate>=0.7.0", - "pyarrow>=16.0.0", + "pyarrow>=23.0.1", "pydantic>=2.8.0", "sqlalchemy>=2.0.0,<2.1", "sqlalchemy-utils>=0.38.0, <0.44", # expanding lowerbound to work with pydoris diff --git a/superset/db_engine_specs/snowflake.py b/superset/db_engine_specs/snowflake.py index dbb8bbb80f1e..771994c28522 100644 --- a/superset/db_engine_specs/snowflake.py +++ b/superset/db_engine_specs/snowflake.py @@ -680,7 +680,7 @@ def update_params_from_encrypted_extra( logger.error(ex, exc_info=True) raise auth_method = encrypted_extra.get("auth_method", None) - auth_params = encrypted_extra.get("auth_params", {}) + auth_params = encrypted_extra.get("auth_params") or {} if not auth_method: return connect_args = params.setdefault("connect_args", {}) @@ -689,11 +689,16 @@ def update_params_from_encrypted_extra( key = None if privatekey_body: key = privatekey_body.encode() - else: + elif auth_params.get("privatekey_path"): with open(auth_params["privatekey_path"], "rb") as key_temp: key = key_temp.read() + else: + raise ValueError( + "Either 'privatekey_body' or 'privatekey_path' must be provided " + "when using 'keypair' authentication" + ) privatekey_pass = auth_params.get("privatekey_pass", None) - password = privatekey_pass.encode() if privatekey_pass is not None else None + password = privatekey_pass.encode() if privatekey_pass else None p_key = serialization.load_pem_private_key( key, password=password, diff --git a/tests/unit_tests/db_engine_specs/test_snowflake.py b/tests/unit_tests/db_engine_specs/test_snowflake.py index 7164d9b2d8b0..b8d1f1e453de 100644 --- a/tests/unit_tests/db_engine_specs/test_snowflake.py +++ b/tests/unit_tests/db_engine_specs/test_snowflake.py @@ -18,7 +18,7 @@ # pylint: disable=import-outside-toplevel from datetime import datetime -from typing import Optional +from typing import Any, Optional from unittest import mock import pytest @@ -879,3 +879,239 @@ def test_extended_aggregation_func_median_uses_native_snowflake_syntax() -> None for aggregate in ("STDDEV_SAMP", "VAR_SAMP"): assert SnowflakeEngineSpec.get_extended_aggregation_func(aggregate) is not None + + +def test_snowflake_parameters_json_schema() -> None: + """ + Verify parameters_json_schema constructs OpenAPI schema via + APISpec and MarshmallowPlugin. + """ + from superset.db_engine_specs.snowflake import SnowflakeEngineSpec + + schema = SnowflakeEngineSpec.parameters_json_schema() + assert isinstance(schema, dict) + assert "properties" in schema + assert "account" in schema["properties"] + + +def test_snowflake_update_params_from_encrypted_extra_keypair() -> None: + """ + Verify update_params_from_encrypted_extra parses PEM private key and sets DER bytes. + Protects against missing cryptography.hazmat imports. + """ + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + from superset.db_engine_specs.snowflake import SnowflakeEngineSpec + + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + pem_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + mock_db = mock.MagicMock() + mock_db.encrypted_extra = json.dumps( + { + "auth_method": "keypair", + "auth_params": { + "privatekey_body": pem_bytes.decode("utf-8"), + }, + } + ) + + params: dict[str, Any] = {} + SnowflakeEngineSpec.update_params_from_encrypted_extra(mock_db, params) + + expected_der = private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + assert "connect_args" in params + assert "private_key" in params["connect_args"] + assert params["connect_args"]["private_key"] == expected_der + assert not params["connect_args"]["private_key"].startswith(b"-----BEGIN") + + # Verify that the emitted bytes are valid DER-deserializable PKCS8 bytes + deserialized_key = serialization.load_der_private_key( + params["connect_args"]["private_key"], + password=None, + ) + assert deserialized_key.private_numbers().d == private_key.private_numbers().d + + +def test_snowflake_update_params_from_encrypted_extra_keypair_passphrase() -> None: + """ + Verify update_params_from_encrypted_extra decrypts password-protected PEM + private key and produces unencrypted DER-encoded PKCS8 bytes. + """ + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + from superset.db_engine_specs.snowflake import SnowflakeEngineSpec + + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + passphrase = b"test_secret_passphrase" + encrypted_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(passphrase), + ) + + mock_db = mock.MagicMock() + mock_db.encrypted_extra = json.dumps( + { + "auth_method": "keypair", + "auth_params": { + "privatekey_body": encrypted_pem.decode("utf-8"), + "privatekey_pass": passphrase.decode("utf-8"), + }, + } + ) + + params: dict[str, Any] = {} + SnowflakeEngineSpec.update_params_from_encrypted_extra(mock_db, params) + + expected_der = private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + assert "connect_args" in params + assert "private_key" in params["connect_args"] + assert params["connect_args"]["private_key"] == expected_der + assert not params["connect_args"]["private_key"].startswith(b"-----BEGIN") + + deserialized_key = serialization.load_der_private_key( + params["connect_args"]["private_key"], + password=None, + ) + assert deserialized_key.private_numbers().d == private_key.private_numbers().d + + +@pytest.mark.parametrize("auth_params", [None, {}]) +def test_snowflake_update_params_from_encrypted_extra_null_or_empty_auth_params( + auth_params: Optional[dict[str, Any]], +) -> None: + """ + Verify update_params_from_encrypted_extra raises ValueError when auth_method is + keypair but no key material is supplied in auth_params. + """ + from superset.db_engine_specs.snowflake import SnowflakeEngineSpec + + mock_db = mock.MagicMock() + mock_db.encrypted_extra = json.dumps( + { + "auth_method": "keypair", + "auth_params": auth_params, + } + ) + + params: dict[str, Any] = {} + with pytest.raises( + ValueError, + match="Either 'privatekey_body' or 'privatekey_path' must be provided", + ): + SnowflakeEngineSpec.update_params_from_encrypted_extra(mock_db, params) + + +def test_snowflake_update_params_from_encrypted_extra_keypair_path( + tmp_path: Any, +) -> None: + """ + Verify update_params_from_encrypted_extra reads private key from privatekey_path + and sets connect_args['private_key'] to the expected PKCS8 DER bytes. + """ + from pathlib import Path + + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + from superset.db_engine_specs.snowflake import SnowflakeEngineSpec + + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + pem_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + key_file = Path(tmp_path) / "test_private_key.pem" + key_file.write_bytes(pem_bytes) + + mock_db = mock.MagicMock() + mock_db.encrypted_extra = json.dumps( + { + "auth_method": "keypair", + "auth_params": { + "privatekey_path": str(key_file), + }, + } + ) + + params: dict[str, Any] = {} + SnowflakeEngineSpec.update_params_from_encrypted_extra(mock_db, params) + + expected_der = private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + assert "connect_args" in params + assert "private_key" in params["connect_args"] + assert params["connect_args"]["private_key"] == expected_der + + +@pytest.mark.parametrize("privatekey_pass", ["", None]) +def test_snowflake_update_params_from_encrypted_extra_keypair_empty_passphrase( + privatekey_pass: Optional[str], +) -> None: + """ + Verify update_params_from_encrypted_extra treats empty string and None + passphrase as None, avoiding cryptography TypeError with unencrypted keys. + """ + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + from superset.db_engine_specs.snowflake import SnowflakeEngineSpec + + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + pem_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + mock_db = mock.MagicMock() + mock_db.encrypted_extra = json.dumps( + { + "auth_method": "keypair", + "auth_params": { + "privatekey_body": pem_bytes.decode("utf-8"), + "privatekey_pass": privatekey_pass, + }, + } + ) + + params: dict[str, Any] = {} + SnowflakeEngineSpec.update_params_from_encrypted_extra(mock_db, params) + + assert "connect_args" in params + assert "private_key" in params["connect_args"] + assert isinstance(params["connect_args"]["private_key"], bytes) + assert len(params["connect_args"]["private_key"]) > 0