mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
fix(catalogs): validate extension and preset catalog payload shape
`ExtensionCatalog._fetch_single_catalog` and
`PresetCatalog._fetch_single_catalog` only check that the `extensions` /
`presets` key is *present* in the parsed catalog JSON. They don't check
that the value is a JSON object, and they don't check that the root is
a JSON object at all. A malformed (or compromised) upstream catalog
returning:
{"schema_version": "1.0", "extensions": []}
passes both `"extensions" not in catalog_data` and the subsequent
`response.read()` JSON parse, gets cached on disk, and then crashes
deep inside `_get_merged_extensions` (resp. `_get_merged_packs`) with:
AttributeError: 'list' object has no attribute 'items'
instead of the existing user-facing
`ExtensionError("Invalid catalog format from <url>")` /
`PresetError("Invalid preset catalog format")` that the surrounding
code is clearly trying to produce.
The sibling integration-catalog reader already validates this — see
`src/specify_cli/integrations/catalog.py` where the fetch path
explicitly checks both `isinstance(catalog_data, dict)` and
`isinstance(catalog_data.get("integrations"), dict)` before returning.
This change mirrors that pattern in the extension and preset readers so
the three catalog fetchers stay consistent and a malformed upstream
surfaces as the user-facing error instead of a raw Python traceback.
Adds parametrized regression tests covering:
- root payload is not a JSON object (list, str, int, null)
- root is a dict but `extensions` / `presets` value is the wrong type
(list, str, null, int)
All eight bad-payload shapes now raise the expected catalog error.
This commit is contained in:
@@ -1839,8 +1839,27 @@ class ExtensionCatalog(CatalogStackBase):
|
||||
with self._open_url(entry.url, timeout=10) as response:
|
||||
catalog_data = json.loads(response.read())
|
||||
|
||||
# Validate payload shape before iteration. Checking only key
|
||||
# presence would let a payload like ``{"extensions": []}`` or
|
||||
# ``{"extensions": null}`` slip through here and then crash with
|
||||
# ``AttributeError: 'list' object has no attribute 'items'`` deep
|
||||
# inside ``_get_merged_extensions``. The sibling integration
|
||||
# catalog reader already guards both the root object and the
|
||||
# nested mapping (see ``integrations/catalog.py``); the extension
|
||||
# catalog must stay consistent so a malformed upstream surfaces as
|
||||
# the user-facing ``Invalid catalog format`` error instead of a
|
||||
# raw Python traceback.
|
||||
if not isinstance(catalog_data, dict):
|
||||
raise ExtensionError(
|
||||
f"Invalid catalog format from {entry.url}: expected a JSON object"
|
||||
)
|
||||
if "schema_version" not in catalog_data or "extensions" not in catalog_data:
|
||||
raise ExtensionError(f"Invalid catalog format from {entry.url}")
|
||||
if not isinstance(catalog_data.get("extensions"), dict):
|
||||
raise ExtensionError(
|
||||
f"Invalid catalog format from {entry.url}: "
|
||||
"'extensions' must be a JSON object"
|
||||
)
|
||||
|
||||
# Save to cache
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user