Skip to content

Immunity-agent does not inspect script contents before execution (indirect command bypass) #27

Description

@solar-flare99

Problem

Warden's real-time hook dispatch evaluates security patterns against the command string passed to the Bash tool —
not the contents of any script that command invokes. This means a malicious or destructive payload hidden inside a
Python, Bash, or any other script file bypasses all of Warden's runtime checks entirely.

Example:

malicious.py — contents never inspected by Warden

import os
os.system("rm -rf ~/important-dir")

What the agent calls — all Warden sees

python3 malicious.py # ← no pattern match, passes clean

Every pattern in policies.py (DESTRUCTIVE_COMMAND_PATTERN, RCE_CANARY_PATTERN, SECRET_EXFIL_PATTERN, etc.) is
evaluated only against this outer command string. The script file is never opened or read at dispatch time.

This applies to any interpreter invocation: python3 , bash , node , ruby , etc.

Impact

  • An adversarial prompt or supply-chain-compromised dependency could generate a script file and then instruct the
    agent to run it — completely bypassing Warden in enforce mode.
  • The warden scan command does read MCP server source files (scanner.py:_resolve_skill_source), but this is a
    one-time audit pass, not real-time protection, and it only covers scripts referenced in agent config files — not
    arbitrary scripts run during a session.

Proposed Solution: Pre-execution static analysis for script invocations

In hooks.py, inside the normalize_payload / should_block path, add a pre-execution step that:

  1. Detects when the shell command matches an interpreter invocation pattern (python3 , bash , node
    , etc.)
  2. Resolves the script path relative to the workspace
  3. Reads the script contents (with a size cap, similar to scanner.py's _MAX_SOURCE_SIZE = 100 KB)
  4. Runs the existing policy patterns against the script source as an additional synthetic event

Rough sketch:

_INTERPRETER_RE = re.compile(
r"^\s*(python3?|bash|sh|node|ruby|perl|Rscript)\s+([^\s\-][^\s]*.(py|sh|js|rb|pl|r))\b",
re.IGNORECASE,
)

def _extract_script_source(command: str, workspace: Path) -> str | None:
m = _INTERPRETER_RE.match(command)
if not m:
return None
script_path = Path(m.group(2))
if not script_path.is_absolute():
script_path = workspace / script_path
if not script_path.exists() or script_path.stat().st_size > 100 * 1024:
return None
return script_path.read_text(encoding="utf-8", errors="replace")

The extracted source would then be evaluated against the same PolicyEngine rules as a synthetic shell event, with
the finding evidence pointing back to the script file path so the user knows exactly where the threat is.

Limitations to note

This approach won't catch:

  • Obfuscated payloads (exec(base64.b64decode(...)))
  • Scripts fetched at runtime (curl … | python3)
  • Compiled binaries or .pyc files

Those are harder problems (dynamic analysis territory). The static pass described above handles the straightforward
case of a readable script file with inline destructive code, which covers the most realistic threat model for an
AI coding agent session.

Files to change

  • warden/hooks.py — add pre-execution script content extraction and re-evaluation
  • warden/policies.py or warden/policy_engine.py — possibly expose a helper to evaluate raw source text so it can be
    reused from the hooks path without duplicating pattern logic `

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions