Skip to content

fix(ai): extensible exec-wrapper peeling closes the C2 laundering class (M11)#1264

Merged
ocervell merged 1 commit into
ai-resiliencyfrom
fix/guardrail-wrapper-extensible
Jul 2, 2026
Merged

fix(ai): extensible exec-wrapper peeling closes the C2 laundering class (M11)#1264
ocervell merged 1 commit into
ai-resiliencyfrom
fix/guardrail-wrapper-extensible

Conversation

@ocervell

@ocervell ocervell commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Finding — M11 (P1 / Security)

C2 made shell guardrail checks wrapper-aware, but EXEC_WRAPPERS was a static, hard-coded set. Any exec-wrapper not in that set let an attacker launder a denied command — the wrapper was treated as the leaf and its payload never inspected:

proxychains curl http://evil, firejail rm -rf /, flock /tmp/x curl ..., runuser -c 'curl http://evil', script -c '...' — same bypass class as C2.

Root cause

secator/ai/guardrails.py:354 _peel_wrapper only peeled names in the fixed EXEC_WRAPPERS frozenset (guardrails.py:40); everything else fell through as inner[0] = leaf command.

Fix

  • Broaden EXEC_WRAPPERS with the missing wrappers: flock, runuser, su, script, proxychains, proxychains4, firejail, torsocks, torify, unshare, chrt, taskset, catchsegv.
  • _WRAPPER_ARG_GRAMMAR — a compact, data-driven table (opts_taking_a_value, positional_args_before_cmd, cmd_string_opts) so each wrapper's OWN args are skipped to reach the real leaf, reusing the existing peel loop (no second parser):
    • value-opts: proxychains -f cfg, flock -w N, sudo/runuser/su -u user
    • positional operands: flock <lockfile>, su <user>
    • -- end-of-options boundary
    • -c '<cmd>' command strings (runuser/su/script/flock) are re-parsed (shlex) and re-peeled so the nested payload is checked rather than skipped
  • Config-extensible: CONFIG.addons.ai.exec_wrappers (new field, secator/config.py) EXTENDS the built-in set — config is additive over the security baseline, never shrinks it.
  • Preserves C2 behavior for already-covered wrappers (timeout 60 rmrm), the interpreter ask-gate (timeout 60 bash -c ... keeps bash as leaf), and normal commands.

Tests — TestWrapperPeelingM11 (19, all pass)

  • Proven / shfmt-independent: all _peel_wrapper assertions operate on token lists, and the _exec_wrappers config-extension test — no shfmt needed.
  • Proven-via-stub: 3 check_action integration tests stub extract_commands (same pattern as the existing TestOutputFlagWrites), asserting the peeled leaf's deny/ask fires (proxychains dd → deny, flock /tmp/l dd → deny, firejail rm -rf /tmp/x → ask).
  • shfmt-gated: the pre-existing check_action-based wrapper tests in TestDefaultPermissions require real shfmt/safecmd and fail environmentally on every branch (safecmd swallows the FileNotFoundError → empty parse).

Baseline vs after (full test_ai_guardrails.py): 57 failed / 94 passed → 57 failed / 113 passed. Failure set byte-identical (all shfmt-environmental), zero regressions, +19 new passing.

Residual laundering vectors (flagged, not fixed)

  • Compound payload after -c (runuser -c 'curl x && rm -rf /') — only the first command of the re-parsed string is classified (guardrails.py _peel_wrapper cmd-string branch).
  • Arbitrary user shell aliases / functions; $(...)/backtick command substitution; eval; wrapper binaries under nonstandard names or paths not in the set (config extension is the mitigation).

🤖 Generated with Claude Code

…dering (M11)

C2 peeled a HARD-CODED EXEC_WRAPPERS set; any wrapper outside it (proxychains,
firejail, flock, runuser, script, ...) was treated as the leaf command, so its
payload was never inspected — same laundering class as C2.

- Broaden EXEC_WRAPPERS with the known-missing wrappers (flock, runuser, su,
  script, proxychains/proxychains4, firejail, torsocks/torify, unshare, chrt,
  taskset, catchsegv).
- Add _WRAPPER_ARG_GRAMMAR (data-driven, no per-wrapper if-pile) so each
  wrapper's OWN args are skipped to reach the real leaf: value-opts
  (proxychains -f, flock -w, sudo/runuser/su -u), positional operands
  (flock lockfile, su user), `--` end-of-options, and `-c '<cmd>'` command
  strings (runuser/su/script/flock) which are re-parsed and re-peeled so the
  payload is checked, not skipped.
- Make the set config-extensible via CONFIG.addons.ai.exec_wrappers; config
  EXTENDS the built-in baseline (the security floor), never shrinks it.
- Preserve C2 behavior for already-covered wrappers and normal commands.

Tests: TestWrapperPeelingM11 — _peel_wrapper assertions are shfmt-independent
(token-list domain); check_action integration stubs extract_commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 96dba1b5-dc6b-4375-8582-1a570f43496f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/guardrail-wrapper-extensible

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ocervell ocervell merged commit 772f5cc into ai-resiliency Jul 2, 2026
1 check passed
@ocervell ocervell deleted the fix/guardrail-wrapper-extensible branch July 2, 2026 15:34
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