Skip to content

Add human-in-the-loop confirmation for risky package install scripts#348

Open
lmajano wants to merge 2 commits into
developmentfrom
claude/boxlang-security-prompts-s20wjh
Open

Add human-in-the-loop confirmation for risky package install scripts#348
lmajano wants to merge 2 commits into
developmentfrom
claude/boxlang-security-prompts-s20wjh

Conversation

@lmajano

@lmajano lmajano commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Description

A box.json scripts block can be keyed to install-lifecycle interception points (preInstall, onInstall, postInstall, …). The PackageScripts interceptor fires these automatically and PackageService.runScript() runs each entry through shell.callCommand() — arbitrary CommandBox/native commands. This is the classic supply-chain risk: installing a third‑party package from ForgeBox/git can silently execute attacker-controlled commands with no user awareness.

This PR adds a yes/no human-in-the-loop confirmation before CommandBox auto-runs an install-lifecycle script, printing the exact commands first so the user can decline untrusted code.

How it works

  • PackageService.runScript() gains an automatic flag. Only interceptor-driven runs (PackageScripts.cfc) set it, so explicit run-script invocations are never prompted (the user asked for those).
  • Gating is limited to install/uninstall lifecycle events (preInstall, preInstallAll, onInstall, postInstall, postInstallAll, preUninstall, postUninstall). Benign high-frequency points (preCommand, prePrompt, onCLIStart, …) are untouched, so there is no prompt spam.
  • A single config setting scripts.trustInstallScripts (default false) governs trust:
    • false → interactive shell prompts yes/no; non-interactive shell (CI / no TTY) denies since it can't prompt.
    • true → scripts run without prompting.
  • Trust can be granted three ways, all funnelling through the one setting:
    • config set scripts.trustInstallScripts=true
    • box_config_scripts_trustInstallScripts=true env var (uses the existing config-override mechanism in ConfigService.loadOverrides() — no bespoke env-var handling in code)
    • install --trustScripts (per-invocation; the command arg defaults to the config setting and propagates through recursive dependency installs via interceptData.installArgs.trustScripts)

A non-boolean config value is intentionally not silently coerced — it errors out rather than being treated as false.

Reused building blocks: shell.confirm(), shell.isTerminalInteractive(), configService.getSetting() + the box_config_* override in ConfigService.loadOverrides().

Files changed

  • services/PackageService.cfcautomatic param, gate, isRiskyInstallScript() / confirmScriptExecution() helpers, trustScripts threaded through installPackage().
  • package-commands/interceptors/PackageScripts.cfc — pass automatic=true.
  • package-commands/commands/package/install.cfc--trustScripts flag defaulting to the config setting.
  • services/ConfigService.cfc — register scripts.trustInstallScripts.

No new dependencies.

Once approved, the same change is prepared for the boxlang branch on claude/boxlang-security-prompts-s20wjh-boxlang.

Jira Issues

No Jira issue created yet — please advise if one should be linked before merge.

Type of change

  • New Feature
  • This change requires a documentation update (new scripts.trustInstallScripts config setting)

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Transparency: I was unable to run the CommandBox CLI/test suite in this environment (JRE present but no bootstrapped CLI), so the feature has not been exercised at runtime and no automated tests are attached yet. The logic and every caller of runScript() were reviewed by hand. Happy to add TestBox coverage and docs before merge.

🤖 Generated with Claude Code

Package box.json scripts tied to install-lifecycle interception points
(preInstall, onInstall, postInstall, etc.) are fired automatically and
run arbitrary commands via shell.callCommand. Installing a third-party
package could therefore silently execute attacker-controlled commands.

This adds a yes/no confirmation before CommandBox auto-runs one of these
install-lifecycle scripts, printing the exact commands first so the user
can decline untrusted code.

- PackageService.runScript gains an `automatic` flag; only interceptor-
  driven runs (PackageScripts.cfc) set it, so explicit `run-script`
  invocations are never prompted.
- Gating applies only to install/uninstall lifecycle events, not benign
  high-frequency points (preCommand, prePrompt, etc.).
- On by default. Non-interactive shells (CI/no TTY) deny by default unless
  trusted via config `scripts.trustInstallScriptsNonInteractive`, env var
  COMMANDBOX_TRUST_INSTALL_SCRIPTS, or `install --trustScripts`.
- Master switch `scripts.confirmInstallScripts` (default true) can disable.

Reuses shell.confirm() and shell.isTerminalInteractive().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015g9LPH1Ct6zP17DoDEsBUp
@bdw429s

bdw429s commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

We don't make up random CommandBox env vars generally. We already have an existing system for overriding config settings. So COMMANDBOX_TRUST_INSTALL_SCRIPTS should actually be called box_config_scripts_trustInstallScriptsNonInteractive which is our existing convention. And we shouldn't be checking the values of env vars directly in the code. Just check the config settings. The env var will naturally override the config setting as needed all on its own. When the install command delegates to a recursive install command, it should pass along its trust scripts arg to propagate, and the command arg should just default to the config setting.

I also don't understand the need for both scripts.trustInstallScriptsNonInteractive and scripts.confirmInstallScripts. Can't we just have a single trust flag?

The PackageService isn't really the correct place to put an isTruthy() helper. At best, perhaps in the config service since we'd likely want to use this for any boolean config setting which could have been set to a non-boolean value. That said, we've never messed with this level of validation before. If people put a crap value in, we error out instead of just silently ignoring it (and treating it has false). I'm not a fan of the silent treatment. In this specific case, treating an unknown value as false may be the "safe" option, but in another scenario, it could be the opposite.

Also, all of this needs sent to the boxlang branch as well. Either in another pull, or by cherry picking the commit afterward.

@bdw429s bdw429s self-requested a review June 25, 2026 22:41

@bdw429s bdw429s left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stupid GitHub won't let me submit this dumb form without a comment

- Collapse the two settings into a single `scripts.trustInstallScripts`
  (default false). When true, install scripts run without prompting.
- Stop reading a bespoke COMMANDBOX_TRUST_INSTALL_SCRIPTS env var; the
  existing box_config_scripts_trustInstallScripts mechanism overrides the
  config setting automatically. Remove the isTruthy helper so a bad
  boolean value errors out instead of being silently treated as false.
- `install --trustScripts` now defaults to the config setting and is
  threaded through installPackage's recursive dependency installs;
  confirmScriptExecution reads trust from interceptData.installArgs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015g9LPH1Ct6zP17DoDEsBUp
@lmajano

lmajano commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@bdw429s can you review again and if all ok I can send another PR to the boxlang branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants