From 69fa8dda10eb5da7c2383c3b15bb8fbef3b35d21 Mon Sep 17 00:00:00 2001 From: Noor-ul-ain001 Date: Tue, 25 Aug 2026 19:45:20 +0500 Subject: [PATCH] fix(bundler): reject non-string catalog entry tag members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _parse_tags in bundler/models/catalog.py rejected a non-list `tags` value but silently coerced individual non-string members via `str(t) for t in value`, letting a catalog entry like `tags: [1, true, {}]` through as `("1", "True", "{}")` instead of raising. This is the exact sibling of the bug just fixed for the manifest's `_parse_str_list` in #4091 (bundler/models/manifest.py) — same file family, same shape, just not swept across both call sites. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FW9fAYsCBCAgdKWovtSyqt --- src/specify_cli/bundler/models/catalog.py | 11 ++++++++--- tests/contract/test_catalog_schema.py | 9 +++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/bundler/models/catalog.py b/src/specify_cli/bundler/models/catalog.py index 53e83a52e7..59a45bcb1d 100644 --- a/src/specify_cli/bundler/models/catalog.py +++ b/src/specify_cli/bundler/models/catalog.py @@ -106,10 +106,11 @@ def to_dict(self) -> dict[str, Any]: def _parse_tags(value: Any, entry_id: str) -> tuple[str, ...]: - """Coerce a catalog entry's ``tags`` into a tuple of strings. + """Parse a catalog entry's ``tags`` into a tuple of strings. Catalogs are untrusted input: a bare string would otherwise be iterated - character-by-character, so reject anything that is not a list/tuple. + character-by-character, so reject anything that is not a list/tuple, and + reject any non-string member instead of silently coercing it. """ if value is None: return () @@ -117,7 +118,11 @@ def _parse_tags(value: Any, entry_id: str) -> tuple[str, ...]: raise BundlerError( f"Catalog entry '{entry_id}': 'tags' must be a list of strings." ) - return tuple(str(t) for t in value) + if any(not isinstance(item, str) for item in value): + raise BundlerError( + f"Catalog entry '{entry_id}': 'tags' must be a list of strings." + ) + return tuple(value) def _parse_verified(value: Any, entry_id: str) -> bool: diff --git a/tests/contract/test_catalog_schema.py b/tests/contract/test_catalog_schema.py index 15a844118b..0e360ac1aa 100644 --- a/tests/contract/test_catalog_schema.py +++ b/tests/contract/test_catalog_schema.py @@ -238,6 +238,15 @@ def test_catalog_entry_rejects_string_tags(): CatalogEntry.from_dict(data) +def test_catalog_entry_rejects_non_string_tag_members(): + from specify_cli.bundler.models.catalog import CatalogEntry + + data = catalog_entry_dict("demo") + data["tags"] = ["valid", 1] + with pytest.raises(BundlerError, match="'tags' must be a list of strings"): + CatalogEntry.from_dict(data) + + def test_catalog_entry_rejects_non_boolean_verified(): from specify_cli.bundler.models.catalog import CatalogEntry