You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/02 03:37:25 UTC
(superset) 01/01: feat(extensions): add EXTENSION_BLOCKLIST to refuse loading extensions
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch feat/extension-blocklist
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 677c7c7a2e9b957e57e5e13c66be51addd4fd506
Author: Claude Code <no...@anthropic.com>
AuthorDate: Mon Jun 1 20:37:04 2026 -0700
feat(extensions): add EXTENSION_BLOCKLIST to refuse loading extensions
Extensions execute arbitrary Python; once a valid .supx/local extension is
discovered it is loaded unconditionally. Add an EXTENSION_BLOCKLIST config so
operators can refuse a specific extension (by id, blocking all versions) or a
specific version ("<id>@<version>"), even if present in LOCAL_EXTENSIONS or
EXTENSIONS_PATH. Blocked extensions are skipped with a warning in both load
paths.
This is the self-contained slice of the extension supply-chain hardening; a
full vulnerability-database / dependency-audit integration is out of scope.
Add a unit test for is_extension_blocked (id and id@version matching).
Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
---
superset/config.py | 5 ++++
superset/extensions/utils.py | 32 ++++++++++++++++++++
tests/unit_tests/extensions/test_utils.py | 50 +++++++++++++++++++++++++++++++
3 files changed, 87 insertions(+)
diff --git a/superset/config.py b/superset/config.py
index 2a29ce87147..52e471b6388 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -2525,6 +2525,11 @@ except ImportError:
LOCAL_EXTENSIONS: list[str] = []
EXTENSIONS_PATH: str | None = None
+# Extensions that must not be loaded, even if present in LOCAL_EXTENSIONS or
+# EXTENSIONS_PATH. Each entry is an extension id (blocks every version) or
+# "<id>@<version>" (blocks a specific version). Use this to disable an
+# extension found to be vulnerable or otherwise undesirable.
+EXTENSION_BLOCKLIST: list[str] = []
# Default polling interval for tasks (seconds)
TASK_ABORT_POLLING_DEFAULT_INTERVAL = 10
diff --git a/superset/extensions/utils.py b/superset/extensions/utils.py
index 6332c97435d..53d791f6804 100644
--- a/superset/extensions/utils.py
+++ b/superset/extensions/utils.py
@@ -254,6 +254,20 @@ def build_extension_data(extension: LoadedExtension) -> dict[str, Any]:
return extension_data
+def is_extension_blocked(extension: LoadedExtension) -> bool:
+ """
+ Return True if the extension is denied by the ``EXTENSION_BLOCKLIST`` config.
+
+ Each blocklist entry is either an extension id (blocks every version of that
+ extension) or ``"<id>@<version>"`` (blocks only that exact version).
+ """
+ blocklist = set(current_app.config.get("EXTENSION_BLOCKLIST") or [])
+ if not blocklist:
+ return False
+ manifest = extension.manifest
+ return manifest.id in blocklist or f"{manifest.id}@{manifest.version}" in blocklist
+
+
def get_extensions() -> dict[str, LoadedExtension]:
extensions: dict[str, LoadedExtension] = {}
@@ -265,6 +279,15 @@ def get_extensions() -> dict[str, LoadedExtension]:
abs_dist_path = str((Path(path) / "dist").resolve())
extension = get_loaded_extension(files, source_base_path=abs_dist_path)
extension_id = extension.manifest.id
+ if is_extension_blocked(extension):
+ logger.warning(
+ "Refusing to load blocklisted extension %s (ID: %s, "
+ "version: %s) from local filesystem",
+ extension.name,
+ extension_id,
+ extension.manifest.version,
+ )
+ continue
extensions[extension_id] = extension
logger.info(
"Loading extension %s (ID: %s) from local filesystem",
@@ -280,6 +303,15 @@ def get_extensions() -> dict[str, LoadedExtension]:
for extension in discover_and_load_extensions(extensions_path):
extension_id = extension.manifest.id
+ if is_extension_blocked(extension):
+ logger.warning(
+ "Refusing to load blocklisted extension %s (ID: %s, "
+ "version: %s) from discovery path",
+ extension.name,
+ extension_id,
+ extension.manifest.version,
+ )
+ continue
if extension_id not in extensions: # Don't override LOCAL_EXTENSIONS
extensions[extension_id] = extension
logger.info(
diff --git a/tests/unit_tests/extensions/test_utils.py b/tests/unit_tests/extensions/test_utils.py
new file mode 100644
index 00000000000..545c7cee800
--- /dev/null
+++ b/tests/unit_tests/extensions/test_utils.py
@@ -0,0 +1,50 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from unittest.mock import MagicMock
+
+from flask import current_app
+
+from superset.extensions.utils import is_extension_blocked
+
+
+def _extension(ext_id: str, version: str) -> MagicMock:
+ extension = MagicMock()
+ extension.manifest.id = ext_id
+ extension.manifest.version = version
+ return extension
+
+
+def test_is_extension_blocked() -> None:
+ original = current_app.config.get("EXTENSION_BLOCKLIST")
+ try:
+ # Empty blocklist: nothing is blocked.
+ current_app.config["EXTENSION_BLOCKLIST"] = []
+ assert is_extension_blocked(_extension("acme.widget", "1.0.0")) is False
+
+ # Block by id: every version of that id is blocked.
+ current_app.config["EXTENSION_BLOCKLIST"] = ["acme.widget"]
+ assert is_extension_blocked(_extension("acme.widget", "1.0.0")) is True
+ assert is_extension_blocked(_extension("acme.widget", "2.0.0")) is True
+ assert is_extension_blocked(_extension("acme.other", "1.0.0")) is False
+
+ # Block by id@version: only that exact version is blocked.
+ current_app.config["EXTENSION_BLOCKLIST"] = ["acme.widget@1.0.0"]
+ assert is_extension_blocked(_extension("acme.widget", "1.0.0")) is True
+ assert is_extension_blocked(_extension("acme.widget", "2.0.0")) is False
+ finally:
+ current_app.config["EXTENSION_BLOCKLIST"] = original