Skip to content

Commit aa810ba

Browse files
escape user-controlled values in integration-options error messages
the malformed-quoting handler (and the unexpected/unknown option branches) interpolate raw_options/token into console.print. a value carrying an unbalanced rich tag like '--commands-dir "[/red]foo' first trips the intended shlex ValueError, but the error print then raises rich.errors.MarkupError and leaks a traceback anyway. escape all three before printing so the clean typer.Exit survives. added a regression covering both the shlex path and a bare markup token.
1 parent 3183fd1 commit aa810ba

2 files changed

Lines changed: 32 additions & 4 deletions

File tree

src/specify_cli/integrations/_helpers.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any, Callable
77

88
import typer
9+
from rich.markup import escape
910

1011
from .._agent_config import SCRIPT_TYPE_CHOICES
1112
from .._console import console
@@ -196,10 +197,12 @@ def _parse_integration_options(integration: Any, raw_options: str) -> dict[str,
196197
# shlex raises on malformed input (e.g. an unbalanced quote in
197198
# --integration-options='--commands-dir "foo'). Surface the clean CLI
198199
# error every other bad-input path here produces instead of leaking a
199-
# raw ValueError traceback.
200+
# raw ValueError traceback. raw_options is user-controlled, so escape
201+
# it before interpolating — an unbalanced Rich tag like '[/red]' would
202+
# otherwise raise MarkupError inside console.print and leak a traceback.
200203
console.print(
201204
f"[red]Error:[/red] Could not parse integration options "
202-
f"'{raw_options}': {error}."
205+
f"'{escape(raw_options)}': {escape(str(error))}."
203206
)
204207
raise typer.Exit(1) from error
205208
declared_options = list(integration.options())
@@ -209,7 +212,7 @@ def _parse_integration_options(integration: Any, raw_options: str) -> dict[str,
209212
while i < len(tokens):
210213
token = tokens[i]
211214
if not token.startswith("-"):
212-
console.print(f"[red]Error:[/red] Unexpected integration option value '{token}'.")
215+
console.print(f"[red]Error:[/red] Unexpected integration option value '{escape(token)}'.")
213216
if allowed:
214217
console.print(f"Allowed options: {allowed}")
215218
raise typer.Exit(1)
@@ -220,7 +223,7 @@ def _parse_integration_options(integration: Any, raw_options: str) -> dict[str,
220223
name, value = name.split("=", 1)
221224
opt = declared.get(name)
222225
if not opt:
223-
console.print(f"[red]Error:[/red] Unknown integration option '{token}'.")
226+
console.print(f"[red]Error:[/red] Unknown integration option '{escape(token)}'.")
224227
if allowed:
225228
console.print(f"Allowed options: {allowed}")
226229
raise typer.Exit(1)

tests/integrations/test_integration_subcommand.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2693,6 +2693,31 @@ def test_malformed_quoting_exits_cleanly(self):
26932693
with pytest.raises(typer.Exit):
26942694
_parse_integration_options(integration, '--commands-dir "foo')
26952695

2696+
def test_malformed_quoting_with_rich_markup_exits_cleanly(self):
2697+
"""A malformed value carrying Rich markup must still exit cleanly.
2698+
2699+
raw_options is user-controlled. A value like '--commands-dir "[/red]foo'
2700+
first trips the shlex ValueError path, but the error message then
2701+
interpolates the raw value into console.print — an unbalanced Rich tag
2702+
such as '[/red]' would raise rich.errors.MarkupError there and leak a
2703+
traceback anyway. The value must be escaped so the clean typer.Exit
2704+
survives."""
2705+
import typer
2706+
2707+
from specify_cli.integrations._commands import _parse_integration_options
2708+
from specify_cli.integrations import get_integration
2709+
2710+
integration = get_integration("generic")
2711+
assert integration is not None
2712+
2713+
# Unbalanced quote (shlex path) + markup injection in one value.
2714+
with pytest.raises(typer.Exit):
2715+
_parse_integration_options(integration, '--commands-dir "[/red]foo')
2716+
2717+
# Markup injection in a token that parses but is unexpected/unknown.
2718+
with pytest.raises(typer.Exit):
2719+
_parse_integration_options(integration, "[/red]foo")
2720+
26962721

26972722
class TestUninstallNoManifestClearsInitOptions:
26982723
def test_init_options_cleared_on_no_manifest_uninstall(self, tmp_path):

0 commit comments

Comments
 (0)