Skip to content

fix(core): harden portable shell authorization - #44016

Open
kitlangton wants to merge 4 commits into
v2from
harden-shell-scan
Open

fix(core): harden portable shell authorization#44016
kitlangton wants to merge 4 commits into
v2from
harden-shell-scan

Conversation

@kitlangton

@kitlangton kitlangton commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What

Harden the opt-in portable shell permission scanner so uncertain shell input cannot execute under a narrower saved approval.

This PR builds on the behavior-preserving scanner relocation merged in #44026.

Before / After

Before: effectful syntax such as assignment-only redirects and lone PowerShell carriage returns could produce no shell resources. Opaque input could inherit a prefix approval such as echo *, and unknown directory changes could bypass scoped external-directory denies or be resolved against the wrong environment.

After: unsupported or uncertain input requires a one-time exact command approval plus conservative unknown-directory authorization. Scoped wildcard allows cannot authorize opaque input, scoped denies still apply, and opaque approvals cannot persist wildcard-shaped grants. Static directory analysis now uses the invocation environment and fails closed for disguised, sequential, wrapped, state-mutating, startup-hook, or otherwise unresolved changes.

How

  • packages/core/src/shell/scan.ts rejects side-effecting assignment statements, compound redirects, runtime PowerShell alias mutation, unsupported parameter syntax, unsafe quoting, and other under-reported forms.
  • packages/core/src/shell/parse.ts carries analysis certainty, uses the actual invocation environment, recognizes additional directory command forms, preserves lexical save prefixes, and marks startup hooks or same-command environment mutation uncertain.
  • packages/core/src/permission.ts adds an internal exact-resource evaluation mode that ignores scoped wildcard allows while conservatively retaining configured denies.
  • packages/core/src/tool/plugin/shell.ts makes opaque shell and unknown-directory prompts non-persistable.
  • Permission documentation describes supported shell families and fail-closed behavior.
  • The changeset releases @opencode-ai/core only; the scanner is no longer a package after refactor(core): inline portable shell scanner #44026.

Scope

Testing

  • packages/core: focused scanner, parser, permission, and shell-tool suite (381 passed)
  • packages/core: bun typecheck, bun run build
  • packages/www: bun typecheck, bun validate, bun run build
  • Repository: bun run lint (0 errors, existing warnings only), git diff --check
  • Push hook repository typecheck: 32 successful packages
  • Complete Core suite: 2,278 passed, 16 skipped

Flow

flowchart TD
  Input[Shell input] --> Scan{Portable analysis complete?}
  Scan -->|yes| Resources[Derived command and directory resources]
  Scan -->|no| Exact[Exact raw command, no saved approval]
  Exact --> Unknown[Unknown external directory, no saved approval]
  Resources --> Policy[Normal wildcard policy evaluation]
  Unknown --> Strict[Conservative exact-mode policy evaluation]
  Exact --> Strict
  Policy --> Execute{Allowed?}
  Strict --> Execute
  Execute -->|yes| Run[Spawn shell]
  Execute -->|no| Stop[Reject before spawn]
Loading

@kitlangton
kitlangton changed the base branch from v2 to shell-scan-core August 21, 2026 23:55
Base automatically changed from shell-scan-core to v2 August 22, 2026 00:06
@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

  1. packages/core/src/permission.ts:186 — In evaluated() exact mode, the condition rule.resource === resource || rule.resource === "*" || rule.effect !== "allow" returns any ask/deny rule whose action matches, even when the rule's resource pattern doesn't cover the requested resource. Why it matters: a scoped config like deny external_directory /secrets/** becomes an action-wide deny for all exact-mode requests (["*"]), diverging from wildcard-mode semantics where resource patterns gate the effect. Suggestion: only return a rule when rule.resource === resource || rule.resource === "*"; otherwise continue for every effect, falling through to the ask default — i.e. drop the || rule.effect !== "allow" arm.

  2. packages/core/tool/plugin/shell.ts (behavioral) — After a user approves an unknown-directory prompt with "always allow" (saved against a concrete path), later exact-mode asserts use resources: ["*"], which no saved concrete-path allow can ever match (and evaluated skips wildcard-allow rules). Why it matters: "don't ask again" silently degrades to ask-every-time for subsequent opaque commands. Possibly intended ("unknown dirs can't inherit narrower approvals"), but it should be stated in the changeset/permissions.mdx so users understand the persistent prompting.

  3. packages/core/src/permission.ts:186 (test gap) — evaluated() is a pure function carrying the subtlest logic in this PR, yet it's only exercised indirectly. Suggestion: add a direct matrix test: exact-vs-wildcard allow, "*" allow hit, wildcard-allow skip, non-matching deny (pins item 1's chosen semantics), non-matching ask, and empty-ruleset default.

  4. packages/core/src/shell/parse.ts:384 — popd/pushd now unconditionally report unknown: true, and any second directory change flips directoryUnknown — correct and conservative, but note the whole command string then goes out as a single opaque resource with save: [] (shell.ts), losing the per-command breakdown in approval history/audit. Suggestion: keep the strictness, but consider still resolving/publishing the known directories for observability alongside the blanket prompt.

  5. packages/core/src/shell/parse.ts:795 — ~ now expands via $HOME only; on typical Windows setups (no HOME, PowerShell default), cd ~ becomes unknown ⇒ prompt. Fail-closed is right, but mapping USERPROFILE as a fallback for the portable path would avoid a noisy UX regression on Windows while staying deterministic.

  6. packages/core/src/shell/scan.ts:498 (nit) — The curly-quote blocklist covers U+2018–U+201D; other confusables (fullwidth "', angle quotes «»‹›) pass through to word parsing. Low risk since they'd rarely execute, but a broader "non-ASCII quote-like" class would be cheaper than enumerating.

Nice work overall — the BASH_ENV/BASH_FUNC_* startup gating, alias-command opacity in PowerShell, and the adversarial/generated test suites close real bypass classes.

— automated review (ox-alpha, round2)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants