From 9cbb985b5fd58b1d1d747316ebb4d223363e5374 Mon Sep 17 00:00:00 2001 From: Tang Vu <145498528+tang-vu@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:03:51 +0700 Subject: [PATCH] fix(extensions): validate catalog payload shape --- .../services/dashboard-api/config.py | 15 ++++++++++-- .../dashboard-api/tests/test_config.py | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ods/extensions/services/dashboard-api/config.py b/ods/extensions/services/dashboard-api/config.py index 9532fccea..bb80382de 100644 --- a/ods/extensions/services/dashboard-api/config.py +++ b/ods/extensions/services/dashboard-api/config.py @@ -560,8 +560,19 @@ def load_extension_catalog() -> list[dict]: return [] try: data = json.loads(CATALOG_PATH.read_text(encoding="utf-8")) - return data.get("extensions", []) - except (json.JSONDecodeError, OSError) as e: + if not isinstance(data, dict): + raise ValueError("catalog root must be an object") + extensions = data.get("extensions", []) + if not isinstance(extensions, list): + raise ValueError("catalog extensions must be a list") + valid_extensions = [item for item in extensions if isinstance(item, dict)] + if len(valid_extensions) != len(extensions): + logger.warning( + "Skipped %d non-object extension catalog entries", + len(extensions) - len(valid_extensions), + ) + return valid_extensions + except (json.JSONDecodeError, OSError, ValueError) as e: logger.warning("Failed to load extensions catalog: %s", e) return [] diff --git a/ods/extensions/services/dashboard-api/tests/test_config.py b/ods/extensions/services/dashboard-api/tests/test_config.py index f6a67fb9d..da6253c99 100644 --- a/ods/extensions/services/dashboard-api/tests/test_config.py +++ b/ods/extensions/services/dashboard-api/tests/test_config.py @@ -1,5 +1,6 @@ """Tests for config.py — manifest loading and service discovery.""" +import json import logging from pathlib import Path @@ -68,6 +69,29 @@ def test_live_env_value_preserves_explicit_empty_value(monkeypatch, tmp_path): assert config.read_live_env_value("LEMONADE_MODEL", "fallback") == "" +@pytest.mark.parametrize("payload", [[], {"extensions": {}}, "invalid"]) +def test_extension_catalog_rejects_invalid_payload_shape( + monkeypatch, tmp_path, payload +): + catalog = tmp_path / "extensions-catalog.json" + catalog.write_text(json.dumps(payload), encoding="utf-8") + monkeypatch.setattr(config, "CATALOG_PATH", catalog) + + assert config.load_extension_catalog() == [] + + +def test_extension_catalog_skips_non_object_entries(monkeypatch, tmp_path): + catalog = tmp_path / "extensions-catalog.json" + valid = {"id": "qdrant", "name": "Qdrant"} + catalog.write_text( + json.dumps({"extensions": [None, "invalid", valid]}), + encoding="utf-8", + ) + monkeypatch.setattr(config, "CATALOG_PATH", catalog) + + assert config.load_extension_catalog() == [valid] + + class TestReadManifestFile: def test_reads_yaml(self, tmp_path):