Back to skill

Security audit

Enterprise Legal Guardrails Public

Security checks for vulnerabilities and agentic risk

Overview

This skill is a disclosed outbound guardrail tool, but its optional command-running wrapper has unsafe defaults and escape hatches that warrant careful review before installation.

Install only if you need both the policy checker and the command wrapper. For production use, prefer direct checker-only mode or require --strict, absolute-path allowlists, --sanitize-env with narrow keep rules, no --allow-any-command, and careful audit-log handling because command arguments and inherited credentials can be sensitive.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/guard_and_run.py:366
Finding
Human-review findings are permitted to execute by default<![CDATA[ ## Vulnerability Details **File Location**: `scripts/guard_and_run.py:366-376, 823-835` **Vulnerability Type**: Fail-open handling of review-required content **Risk Level**: High ### Vulnerable Code ```python blocked = status == "BLOCK" or (strict and status == "REVIEW") if blocked: print( f"Blocked by enterprise legal guardrails ({status}) for {action} on {app or 'unknown'} " f"before command execution. Score: {report.get('score', 'n/a')}, " f"Findings: {report.get('findings_count', 'n/a')}", file=sys.stderr, ) return report, True, duration_ms if status == "REVIEW": suggestion = (report.get("suggestions") or ["Consider rewriting before execution."])[0] print(f"Guardrail REVIEW for {action} on {app or 'unknown'}: {suggestion}", file=sys.stderr) return report, False, duration_ms ``` The returned `False` allows execution to continue: ```python env = None if args.sanitize_env: env = _sanitize_env(args.keep_env, args.keep_env_prefix) command_ms = None command_start = time.perf_counter() try: proc = subprocess.run(command, check=False, env=env, timeout=args.command_timeout) ``` ### Technical Analysis The wrapper only treats `REVIEW` as blocking when optional strict mode is enabled. In the default configuration, `REVIEW` generates a warning and returns `blocked=False`, after which the wrapped outbound command is executed. This behavior conflicts with the documented workflow in `SKILL.md:24-26`, which states that only `PASS` and `WATCH` should proceed and that `REVIEW` content should be rewritten or routed for human or legal review. The behavior is intentional enough to be covered by `scripts/tests_guard_and_run.py:54-68`, where a REVIEW result is expected to execute successfully. A safety boundary intended to prevent risky publication should fail closed when human review is required. Making strict mode optional permits potentially defamatory, privacy-sensitive, legally risky, or mis ...[truncated 1135 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat `REVIEW` as blocking by default: ```python blocked = status in {"REVIEW", "BLOCK"} ``` 2. If REVIEW execution is operationally necessary, require a dedicated override rather than relying on a permissive default. 3. Protect the override with authenticated approval, a ticket or case reference, approver identity, expiration, and immutable audit logging. 4. Do not reuse the command-allowlist override as legal-review approval; these controls address different risks. 5. Update tests so default REVIEW behavior asserts that the command does not run. 6. Update documentation and exit-code behavior to consistently distinguish policy blocks, review requirements, and execution failures. 7. Consider requiring strict mode unconditionally for outbound commands that publish publicly, disclose personal information, or use production credentials. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/guard_and_run.py:107
Finding
Executable-name allowlist can be bypassed through PATH substitution<![CDATA[ ## Vulnerability Details **File Location**: `scripts/guard_and_run.py:107-145, 823-835` **Vulnerability Type**: Unsafe executable resolution after basename-only allowlist validation **Risk Level**: High ### Vulnerable Code ```python def _is_allowed(command: list[str], allowed: list[str]) -> bool: if not allowed: return True target = command[0] target_name = Path(target).name target_lower = target.lower() target_name_lower = target_name.lower() for pattern in allowed: if not pattern: continue pattern = pattern.strip() if not pattern: continue candidate = pattern.lower() if candidate.startswith("regex:"): expr = candidate.split(":", 1)[1] try: if re.fullmatch(expr, target_lower): return True except re.error as exc: raise RuntimeError(f"Invalid regex allowlist pattern '{pattern}': {exc}") from exc continue if fnmatch.fnmatch(target_lower, candidate) or fnmatch.fnmatch(target_name_lower, candidate): return True if Path(pattern).is_absolute(): try: if Path(target).resolve() == Path(pattern).resolve(): return True except OSError: if target_lower == pattern.lower(): return True elif target_lower == pattern.lower() or target_name_lower == pattern.lower(): return True return False ``` The validated command name is subsequently executed using environment-based path resolution: ```python env = None if args.sanitize_env: env = _sanitize_env(args.keep_env, args.keep_env_prefix) command_ms = None command_start = time.perf_counter() try: proc = subprocess.run(command, check=False, env=env, timeout=args.command_timeout) ``` ### Technical Analysis For a relative executable such as `python3` or `gog`, `_is_ ...[truncated 2133 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require allowlist entries to be canonical absolute executable paths. 2. Before validation, resolve relative commands with `shutil.which()` using a fixed, trusted PATH. 3. Canonicalize the resolved executable with `Path.resolve(strict=True)` and compare that path against canonical allowlist entries. 4. Execute the already-resolved absolute path rather than the original basename. 5. Reject executables located in directories writable by the invoking user, untrusted groups, or all users. 6. Do not preserve an arbitrary inherited PATH in sanitized mode. Configure a minimal trusted value such as `/usr/local/bin:/usr/bin:/bin`, adjusted for the deployment. 7. Consider recording and verifying executable ownership, permissions, and a cryptographic digest for high-assurance deployments. 8. Avoid wildcard or regular-expression executable allowlists unless they are matched against canonical absolute paths and cannot include untrusted directories. 9. Add regression tests that prepend a temporary directory containing a fake allowlisted executable and verify that execution is rejected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/guard_and_run.py:678
Finding
Rejected command arguments can be written to audit logs in plaintext<![CDATA[ ## Vulnerability Details **File Location**: `scripts/guard_and_run.py:222-251, 678-692` **Vulnerability Type**: Sensitive information exposure through audit logging **Risk Level**: Medium ### Vulnerable Code When a command is rejected, the full command and all arguments are formatted into the error message: ```python if not args.allow_any_command and not command_allowed: msg = f"Blocked command '{_command_repr(command)}' because it is not in the allowlist." print(msg, file=sys.stderr) _append_pre_execution_audit( args=args, command=command, text=text, error_stage="command-allowlist", error_kind="preflight.command_not_allowed", error_message=msg, ) return 1 ``` The audit function stores that message without redaction: ```python payload = { "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "app": app or "", "action": action, "status": status, "decision": ( "blocked" if status == "BLOCK" or (strict and status == "REVIEW") or error_kind is not None else "proceed" ), "score": report.get("score", 0), "findings_count": report.get("findings_count", 0), "text_hash": _hash_text(report.get("original_text", "")), "text_len": len(report.get("original_text", "")), "command_hash": _hash_command(command), "command_preview": Path(command[0]).name, "command_ran": command_ran, "dry_run": dry_run, "command_exit_code": command_exit_code, "strict": bool(strict), "allow_any_command": bool(allow_any_command), "allowed_command_count": int(allowed_command_count), "allow_any_command_reason": allow_any_command_reason or "", "allow_any_command_approval_token": _fingerprint_token(allow_any_command_approval_token), "error_kind": error_kind, "error_stage": error_stage, "error_message": error_message, "guardrail_ms": guardrail_ms, "command_ms": command_ms, } log_path = Path ...[truncated 2348 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Never include the complete command in stderr or audit error messages. 2. Replace the rejected-command message with redacted metadata: ```python msg = ( f"Blocked executable '{Path(command[0]).name}' because it is not " f"in the allowlist; argument_count={max(len(command) - 1, 0)}." ) ``` 3. Continue recording only the command hash, executable basename, and argument count. 4. Apply a centralized redaction function to every audit field and error path before serialization. 5. Explicitly mask common secret formats, authorization headers, signed URLs, tokens, passwords, and personal identifiers. 6. Create audit files with restrictive permissions, such as mode `0600`, rather than relying on the process umask. 7. Validate that the audit path is not a symbolic link and use secure file-opening flags where supported. 8. Establish log retention, access-control, encryption, and rotation policies appropriate for potentially sensitive operational metadata. 9. Add tests that submit recognizable secret values in rejected command arguments and assert that neither stderr nor the audit file contains those values. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (22)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared description frames this as a legal/compliance guardrail, but the documented behavior includes a generic command-execution adapter, environment propagation controls, allowlist bypasses, and audit logging. This mismatch is dangerous because users may invoke or approve the skill assuming it only evaluates text, while it can act as an execution boundary that launches local commands, which materially increases attack surface and the risk of misuse or privilege abuse.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
The skill metadata and docstring frame this as enterprise legal/compliance guardrails, but the implementation includes generic local command execution. That mismatch materially increases risk because operators may grant this skill trust and permissions appropriate for policy review, not for arbitrary process execution.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
The `--allow-any-command` path explicitly bypasses the command allowlist and permits execution of any local command when a reason and token are provided, both of which can come from flags or environment variables. For a legal/compliance guardrail skill, this creates a direct arbitrary command execution escape hatch that undermines the control's stated purpose.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def run(*args: str, env: dict[str, str] | None = None, input_text: str | None = None) -> tuple[int, str, str]:
    if env is None:
        base_env = dict(os.environ)
        if not any(
            base_env.get(name)
            for name in (
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
assert "requires delimiter --" in err, (out, err)

# 6b) Missing allowlist by explicit env var should block execution.
env_no_allowlist = {k: v for k, v in os.environ.items() if k not in {
    "ENTERPRISE_LEGAL_GUARDRAILS_ALLOWED_COMMANDS",
    "ELG_ALLOWED_COMMANDS",
    "BABYLON_ALLOWED_COMMANDS",
Confidence
70% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
assert "requires delimiter --" in err, (out, err)

# 6b) Missing allowlist by explicit env var should block execution.
env_no_allowlist = {k: v for k, v in os.environ.items() if k not in {
    "ENTERPRISE_LEGAL_GUARDRAILS_ALLOWED_COMMANDS",
    "ELG_ALLOWED_COMMANDS",
    "BABYLON_ALLOWED_COMMANDS",
Confidence
70% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
"--",
    "cat",
    "/etc/hosts",
    env={**os.environ.copy(), "BABYLON_ALLOWED_COMMANDS": "python3"},
)
assert code == 1, (code, out, err)
assert "not in the allowlist" in err, (out, err)
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
"--",
    "cat",
    "/etc/hosts",
    env={**os.environ.copy(), "BABYLON_ALLOWED_COMMANDS": "python3"},
)
assert code == 1, (code, out, err)
assert "not in the allowlist" in err, (out, err)
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The tested --allow-any-command escape hatch intentionally bypasses the command allowlist when a reason and token are supplied, defeating the primary execution safeguard. In a guardrails skill, this is especially risky because it creates a built-in path from constrained execution to arbitrary command execution if tokens are exposed, guessed, mishandled, or approved too broadly.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
"python3",
        "-c",
        "import os; print('KEEP_ME' in os.environ, any(k.startswith('SHARED_') for k in os.environ), 'DROP_ME' in os.environ)",
        env={
            **os.environ,
            "KEEP_ME": "1",
            "SHARED_TOKEN": "2",
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
# 16) Execution requires explicit enablement unless --dry-run is used.
with tempfile.TemporaryDirectory() as tmpdir:
    log_path = Path(tmpdir) / "execute_disabled.jsonl"
    env_execute_off = {
        **os.environ,
        "ENTERPRISE_LEGAL_GUARDRAILS_EXECUTE": "0",
    }
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill exposes shell, file read/write, and environment-dependent behavior but does not declare an explicit tool scope or permissions boundary in the skill manifest. That creates a trust gap: downstream users and orchestrators may treat it as a passive policy checker, while the documented wrapper can execute commands and handle environment variables, increasing the chance of over-privileged or unintended use.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The manifest says the skill provides legal/compliance guardrails for anti-spam, defamation, privacy, and financial claims, but the code additionally scans for harassment and targeted abuse. While related to moderation, this is a distinct policy domain not represented in the stated purpose, creating a description-to-behavior mismatch.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The manifest describes guardrails for anti-spam, defamation, privacy, and financial claims, but the code also enforces a separate HR-sensitive workplace language policy. This expands the skill from the declared outbound legal/compliance domains into employee-relations content moderation, which is not disclosed in the manifest description.

Intent-Code Divergence

Medium
Confidence
92% confidence
Finding
The documentation minimizes the capability as a guardrail adapter for outbound/public-facing workflows, while the code can launch broad local commands unrelated to outbound actions. This deceptive or incomplete framing increases the chance of unsafe deployment, overtrust, and accidental exposure of powerful execution capability.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
start = time.perf_counter()
    try:
        proc = subprocess.run(
            args,
            text=True,
            capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
command_ms = None
    command_start = time.perf_counter()
    try:
        proc = subprocess.run(command, check=False, env=env, timeout=args.command_timeout)
        command_ms = int((time.perf_counter() - command_start) * 1000)
    except FileNotFoundError:
        msg = f"Command not found: {command[0]}"
Confidence
97% confidence
Finding
This subprocess invocation executes a user-supplied command after only allowlist checks, turning a legal/compliance guardrail skill into a general local command runner. In this skill context, that is dangerous because any misconfiguration, overbroad allowlist, or enabled bypass can lead to arbitrary local code execution under the agent's privileges.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The test suite validates and normalizes behavior for a command-executing adapter far beyond the stated legal/compliance purpose, including command execution, environment passing, allowlist logic, and bypass cases. That widens the operational scope of the skill into a generic execution wrapper, increasing the chance it is reused as an execution primitive in contexts where only content guardrails were expected.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
base_env["ENTERPRISE_LEGAL_GUARDRAILS_EXECUTE"] = "1"

    command = [sys.executable, str(SCRIPT), *args]
    proc = subprocess.run(
        command,
        env=base_env,
        input=input_text,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
env=env_no_allowlist,
)
assert code == 2, (code, out, err)
assert "Refusing --allow-any-command without approval token" in err, (out, err)

# 12d) Unsafe escape hatch: allow any command only when explicitly enabled + reason+token.
code, out, err = run(
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
env=env_no_allowlist,
)
assert code == 2, (code, out, err)
assert "Refusing --allow-any-command without approval token" in err, (out, err)

# 12d) Unsafe escape hatch: allow any command only when explicitly enabled + reason+token.
code, out, err = run(
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Intent-Code Divergence

Low
Confidence
75% confidence
Finding
The top-level docstring presents the tool as a deterministic local scan with no outside dependencies, but the implementation is also parameterized by multiple environment variables, including Babylon-specific app/scope settings. This is not a direct security problem, but the documentation overstates self-containment and omits behavior that materially affects when guardrails apply.

Static analysis

No suspicious patterns detected.