FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

shell: bare redirect bypasses the permission check (scanner returns no commands) · Issue #49948 · anomalyco/opencode · GitHub

shell: bare redirect bypasses the permission check (scanner returns no commands) #49948

Description

What is the issue?

A shell command that the scanner finds no commands in skips the permission check entirely and runs unchecked. A bare redirect is the clearest case: > file is valid POSIX, truncates or creates the file, and parses to zero commands.

packages/core/src/tool/plugin/shell.ts only asserts when the scan produced something:

const parsed = yield* ShellParse.scan(invocation.command, invocation.shell, target.absolute, { portable })

if (parsed.commands.length > 0)
  yield* permission.assert({})

So with

permission:
  shell:
    "*": "deny"

> important.txt still empties the file — no prompt, no denial, no permission record.

Why it happens

Two things line up:

1. Both scanners report zero commands for a bare redirect. Checked against ShellParse.scan directly, which is what the tool calls:

command legacy portable
> victim.txt 0 commands 0 commands
>> victim.txt 0 commands 0 commands
echo hi > out.txt 1 1
rm -rf x 1 1

Command substitutions are handled correctly — FOO=$(whoami) yields 1 — so this is specific to a redirect with no command word.

2. An empty resource list evaluates to allow. In packages/core/src/permission.ts:

const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect)
const effect = effects.includes("deny") ? "deny" : effects.includes("ask") ? "ask" : "allow"

With resources: [], effects is [], so neither branch matches and the result is allow — even under { action: "*", resource: "*", effect: "deny" }. I added an assertion for exactly that in the existing permission test harness and it passes, so the fail-open is real rather than theoretical.

The commands.length > 0 guard means the second condition is never reached from this path today, but it is the reason the guard is load-bearing.

Reproduction

printf 'important data\n' > victim.txt   # 15 bytes
/bin/sh -c '> victim.txt'
wc -c < victim.txt                       # 0

Then the same command through the shell tool with permission.shell "*": "deny" — it is not denied.

Expected behavior

A command the scanner cannot decompose should still be evaluated, using the raw invocation as the resource, so a deny rule applies and an explicit allow can still permit it. Failing closed seems clearly right for a permission boundary.

I have a small patch and can open a PR.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL