mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(extensions): guard non-numeric catalog downloads in search/info rendering (#3710)
* fix(extensions): guard non-numeric catalog downloads in search/info rendering `specify extension search` and `specify extension info <id>` format a catalog entry's `downloads` field with the `:,` thousands separator, guarded only by `is not None`. Catalog payloads are only shape-validated -- individual fields are never type-checked and `_get_merged_extensions` returns raw catalog dicts -- so an entry with a non-numeric `downloads` (e.g. the JSON string "1500", realistic from a community / SPECKIT_CATALOG_URL / project catalog) makes the `:,` format raise `ValueError: Cannot specify ',' with 's'`, aborting the whole command with an uncaught traceback. Group-format `downloads` only when it is actually numeric; otherwise render it as-is. Numeric values (int/float, incl. bool) format identically, so correct catalogs are byte-for-byte unchanged. Every other field in these two renderers is already `str()`-wrapped; this closes the one unguarded field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extensions): escape the non-numeric downloads fallback for Rich markup Address review feedback: the fallback interpolated the untrusted catalog value straight into a Rich-rendered string, so guarding the ``:,`` ValueError just traded it for a MarkupError -- a catalog entry with downloads "[/red]foo" still aborted `extension search`/`info`, and balanced tags could restyle the output. Wrap the fallback in _escape_markup(str(...)) at both sites, matching how every other catalog field in these renderers is already escaped. Numeric values keep the identical ``:,`` branch, so correct catalogs are unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(extensions): escape 'stars' too, in the same stats string Follow-up to the downloads escaping: `stars` is the other catalog-controlled value joined into the same Rich-rendered stats line, and it was still raw -- verified that stars "[/red]x" raises the same MarkupError and aborts `extension info`/`search`. Hardening one of the two adjacent values would have left the reported defect reachable through the sibling field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -790,10 +790,24 @@ def extension_search(
|
||||
|
||||
# Stats
|
||||
stats = []
|
||||
if ext.get('downloads') is not None:
|
||||
stats.append(f"Downloads: {ext['downloads']:,}")
|
||||
if ext.get('stars') is not None:
|
||||
stats.append(f"Stars: {ext['stars']}")
|
||||
downloads = ext.get('downloads')
|
||||
if downloads is not None:
|
||||
# Catalog fields are untrusted; a non-numeric ``downloads``
|
||||
# (e.g. the JSON string "1500") would crash the ``:,`` format
|
||||
# with "Cannot specify ',' with 's'". Only group-format numbers,
|
||||
# and escape the fallback: the joined stats are rendered as Rich
|
||||
# markup, so a value like "[/red]foo" would raise MarkupError
|
||||
# (matching how every other catalog field here is escaped).
|
||||
stats.append(
|
||||
f"Downloads: {downloads:,}"
|
||||
if isinstance(downloads, (int, float))
|
||||
else f"Downloads: {_escape_markup(str(downloads))}"
|
||||
)
|
||||
stars = ext.get('stars')
|
||||
if stars is not None:
|
||||
# Same untrusted-value/Rich-markup hazard as `downloads` above,
|
||||
# in the same joined string.
|
||||
stats.append(f"Stars: {_escape_markup(str(stars))}")
|
||||
if stats:
|
||||
console.print(f" [dim]{' | '.join(stats)}[/dim]")
|
||||
|
||||
@@ -971,10 +985,24 @@ def _print_extension_info(ext_info: dict, manager):
|
||||
|
||||
# Statistics
|
||||
stats = []
|
||||
if ext_info.get('downloads') is not None:
|
||||
stats.append(f"Downloads: {ext_info['downloads']:,}")
|
||||
if ext_info.get('stars') is not None:
|
||||
stats.append(f"Stars: {ext_info['stars']}")
|
||||
downloads = ext_info.get('downloads')
|
||||
if downloads is not None:
|
||||
# Catalog fields are untrusted; a non-numeric ``downloads`` (e.g. the
|
||||
# JSON string "1500") would crash the ``:,`` format with "Cannot
|
||||
# specify ',' with 's'". Only group-format numbers, and escape the
|
||||
# fallback: the joined stats are rendered as Rich markup, so a value
|
||||
# like "[/red]foo" would raise MarkupError (matching how every other
|
||||
# catalog field here is escaped).
|
||||
stats.append(
|
||||
f"Downloads: {downloads:,}"
|
||||
if isinstance(downloads, (int, float))
|
||||
else f"Downloads: {_escape_markup(str(downloads))}"
|
||||
)
|
||||
stars = ext_info.get('stars')
|
||||
if stars is not None:
|
||||
# Same untrusted-value/Rich-markup hazard as `downloads` above, in the
|
||||
# same joined string.
|
||||
stats.append(f"Stars: {_escape_markup(str(stars))}")
|
||||
if stats:
|
||||
console.print(f"[bold]Statistics:[/bold] {' | '.join(stats)}")
|
||||
console.print()
|
||||
|
||||
Reference in New Issue
Block a user