Skip to content

fix(security): scope find's global options so -L/-H/-P cannot bypass the read grant - #687

Open
rkfshakti wants to merge 1 commit into
andrewyng:mainfrom
rkfshakti:fix/readonly-find-global-options-scoping
Open

rkfshakti wants to merge 1 commit into
andrewyng:mainfrom
rkfshakti:fix/readonly-find-global-options-scoping

Conversation

@rkfshakti

Copy link
Copy Markdown
Contributor

Problem

The read-only session grant vets what a command may do, then holds the files it reads to the session's granted roots (OPE-130). For find, _stage_targets treated every token starting with - as the start of find's predicates and breaked out of the path scan:

elif head == "find":
    break  # find's predicates start here; paths precede them

But find's global options (-L, -H, -P, -D, -O, and the BSD -E/-X/-d/-s/-x) also start with - and precede the starting points. With one, read_targets() returned [], so the root check at permissions.py:463-465 passed vacuously and the command auto-ran under read-only command (session grant):

eng = PermissionEngine(workspace_root=ws, session_readonly=True)
eng.evaluate("run_shell", {"command": "find /etc -name shadow"})      # asks — correct
eng.evaluate("run_shell", {"command": "find -L /etc -name shadow"})   # ALLOWED
eng.evaluate("run_shell", {"command": "find -H ~/.ssh -type f"})      # ALLOWED
eng.evaluate("run_shell", {"command": "find -D search ~/.aws"})       # ALLOWED

read_targets("find -L /etc -name shadow")  # [] (vs ['/etc'] unprefixed)

That is exactly the escape OPE-130 was created to close: a grant the user reads as "stop asking about my project files" also covered ~/.aws/credentials and ~/.ssh/id_rsa — and the self-protection floor guards those against modification, not reads.

Fix

Parse find's global options ahead of the path list in a dedicated _find_targets():

  • -H/-L/-P and the BSD -E/-X/-d/-s/-x take no value
  • -D consumes its debugopts value
  • -O carries its level attached (-O3)
  • -f names the starting point itself (BSD)
  • then collect starting points until the first predicate; with none, find defaults to . (inside every session root)

Tests

tests/test_readonly_scoping.py:

  • read_targets names the starting point for every global-option spelling, and still returns nothing when no path is given
  • the session grant asks for the out-of-root prefixed reads (find -L /etc, find -H ~/.ssh, find -P ~/.aws, find -D search ~/.aws, find -O3 /etc) while still auto-running the in-root forms (find -L . -name '*.py', find -D search src)

Full backend suite: 2649 passed, 14 skipped; the only failures are the two pre-existing test_tool_result_cap.py macOS marker-path failures also present on clean main.

…the read grant

find's global options (-L/-H/-P, -D, -O, and the BSD -E/-X/-d/-s/-x) precede the
starting points, but _stage_targets treated every leading dash token as the start
of find's predicates and returned no read targets. With none, the session grant's
root check in permissions.py passed vacuously: 'find -L /etc -name shadow' and
'find -H ~/.ssh -type f' auto-ran under 'read-only command (session grant)' while
the unprefixed forms correctly asked for approval. That is exactly the escape
OPE-130 was created to close.

Parse find's global options ahead of the path list (-D consumes its debugopts
value; -f names the starting point itself), then collect starting points until
the first predicate. The unprefixed behavior is unchanged.

Tests: read_targets now names the starting point for prefixed forms; the session
grant asks for the out-of-root prefixed reads and still auto-runs them inside
the root.
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.

1 participant