From 813eee55d2acad02bd4f23092931107947128104 Mon Sep 17 00:00:00 2001 From: Aeyo Date: Fri, 5 Jun 2026 03:57:12 +0100 Subject: [PATCH] fix: flag package runner execution for approval --- src/spark_cli/security/approval.py | 15 +++++++++++++++ tests/test_cli.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/spark_cli/security/approval.py b/src/spark_cli/security/approval.py index a3a403d98..7ad5628a5 100644 --- a/src/spark_cli/security/approval.py +++ b/src/spark_cli/security/approval.py @@ -338,6 +338,21 @@ def approval_required_for_command(argv: list[str], context: CommandContext | Non confirmation_phrase="approve remote code execution", ) + if ( + first == "npx" + or (first == "npm" and second == "exec") + or (first in {"pnpm", "yarn"} and second == "dlx") + ): + return _decision( + parts, + ctx, + "remote_code_execution", + "critical", + "Command can download and execute package code through a package runner.", + target_display=" ".join(parts[:3]), + confirmation_phrase="approve remote code execution", + ) + if first == "find" and any(part in {"-exec", "-execdir"} for part in lowered): return _decision( parts, diff --git a/tests/test_cli.py b/tests/test_cli.py index 15be9ec91..4271d7f96 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1491,6 +1491,20 @@ def test_approval_classifier_flags_remote_script_execution(self) -> None: self.assertEqual(decision.action_class, "remote_code_execution") self.assertEqual(decision.risk, "critical") + def test_approval_classifier_flags_package_runner_execution(self) -> None: + for command in ( + ["npx", "--yes", "cowsay@latest", "hello"], + ["npm", "exec", "--yes", "cowsay@latest", "--", "hello"], + ["pnpm", "dlx", "cowsay@latest", "hello"], + ["yarn", "dlx", "cowsay@latest", "hello"], + ): + with self.subTest(command=command): + decision = approval_required_for_command(command, CommandContext(hosted=True, non_interactive=True)) + self.assertTrue(decision.requires_approval) + self.assertEqual(decision.action_class, "remote_code_execution") + self.assertEqual(decision.risk, "critical") + self.assertEqual(decision.approval_mode, "blocked") + def test_approval_classifier_does_not_treat_curl_fail_or_telnet_option_as_upload(self) -> None: for command in ( ["curl", "-f", "https://example.test/health"],