Skip to content

Commit 348d0ee

Browse files
jawwad-aliclaude
andcommitted
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>
1 parent 135444f commit 348d0ee

2 files changed

Lines changed: 24 additions & 4 deletions

File tree

src/specify_cli/extensions/_commands.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -803,8 +803,11 @@ def extension_search(
803803
if isinstance(downloads, (int, float))
804804
else f"Downloads: {_escape_markup(str(downloads))}"
805805
)
806-
if ext.get('stars') is not None:
807-
stats.append(f"Stars: {ext['stars']}")
806+
stars = ext.get('stars')
807+
if stars is not None:
808+
# Same untrusted-value/Rich-markup hazard as `downloads` above,
809+
# in the same joined string.
810+
stats.append(f"Stars: {_escape_markup(str(stars))}")
808811
if stats:
809812
console.print(f" [dim]{' | '.join(stats)}[/dim]")
810813

@@ -995,8 +998,11 @@ def _print_extension_info(ext_info: dict, manager):
995998
if isinstance(downloads, (int, float))
996999
else f"Downloads: {_escape_markup(str(downloads))}"
9971000
)
998-
if ext_info.get('stars') is not None:
999-
stats.append(f"Stars: {ext_info['stars']}")
1001+
stars = ext_info.get('stars')
1002+
if stars is not None:
1003+
# Same untrusted-value/Rich-markup hazard as `downloads` above, in the
1004+
# same joined string.
1005+
stats.append(f"Stars: {_escape_markup(str(stars))}")
10001006
if stats:
10011007
console.print(f"[bold]Statistics:[/bold] {' | '.join(stats)}")
10021008
console.print()

tests/test_extensions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4058,6 +4058,20 @@ def test_info_renders_non_numeric_downloads(self, downloads):
40584058
# Must not raise ValueError or rich.errors.MarkupError.
40594059
_print_extension_info(ext_info, manager)
40604060

4061+
def test_info_renders_markup_bearing_stars(self):
4062+
"""``stars`` sits in the same joined stats string as ``downloads`` and is
4063+
equally catalog-controlled, so it must be escaped too."""
4064+
from unittest.mock import MagicMock
4065+
from specify_cli.extensions._commands import _print_extension_info
4066+
4067+
manager = MagicMock()
4068+
manager.registry.is_installed.return_value = False
4069+
ext_info = {
4070+
"name": "Jira", "id": "jira", "version": "1.0.0",
4071+
"description": "desc", "stars": "[/red]x",
4072+
}
4073+
_print_extension_info(ext_info, manager) # must not raise MarkupError
4074+
40614075
@pytest.mark.parametrize("downloads", ["1500", "[/red]foo"])
40624076
def test_search_survives_non_numeric_downloads(self, temp_dir, downloads):
40634077
"""`specify extension search` must not abort when a catalog entry's

0 commit comments

Comments
 (0)