Skip to content

[Bug] SecurityValidator pattern matching bypassed by environment variable prefixed commands #620

@gjaworski86

Description

@gjaworski86

Summary

The SecurityValidator.hook.ts pattern matching can be bypassed by prefixing any blocked command with environment variable assignments. Commands like LANG=C rm -rf / pass through
undetected because the pattern rm -rf does not match when preceded by LANG=C .

Claude Code v2.1.38 (2026-02-10) fixed this same issue in its own built-in permission matching system. However, SecurityValidator.hook.ts performs its own independent pattern matching
against patterns.yaml, and that layer remains vulnerable.

Environment

  • PAI Version: v2.5
  • Claude Code: v2.1.38
  • OS: macOS (Darwin 25.2.0), likely affects all platforms

Steps to Reproduce

  1. Add a blocked pattern in patterns.yaml, e.g. pattern matching rm -rf
  2. Run: LANG=C rm -rf /some/path
  3. SecurityValidator allows the command because matchesPattern() receives the full string including the env prefix
  4. The pattern rm -rf does not match LANG=C rm -rf /some/path at the expected position

Additional bypass variants:

  • A=1 B=2 dangerous-command (multiple env vars)
  • FOO="bar baz" dangerous-command (quoted values)
  • SPACED=val dangerous-command (leading whitespace)

Expected Behavior

LANG=C rm -rf / should be caught by any pattern that matches rm -rf, regardless of environment variable prefixes.

Actual Behavior

The handleBash() function passes the raw command string directly to validateBashCommand() without stripping env var prefixes. Since regex patterns in patterns.yaml typically match
the command name (e.g., rm -rf), the env prefix causes the match to fail.

Relevant Code

SecurityValidator.hook.ts, handleBash() function:

// Current code - vulnerable
const command = typeof input.tool_input === 'string'
  ? input.tool_input
  : (input.tool_input?.command as string) || '';

const result = validateBashCommand(command);  // raw command with env prefix

Suggested Fix

Two changes in SecurityValidator.hook.ts:

  1. Add normalization function (before the // Pattern Matching section):
  // ========================================
  // Command Normalization
  // ========================================

  /**
   * Strip leading environment variable assignments from a command.
   * Prevents bypass like: LANG=C rm -rf /
   * where "rm -rf /" would not match patterns if the env prefix is present.
   * Handles multiple chained assignments: A=1 B=2 actual-command args
   */
  function stripEnvPrefix(command: string): string {
    return command.replace(/^(\s*[A-Za-z_][A-Za-z0-9_]*=(('[^']*'|"[^"]*"|[^\s])*)\s+)+/, '');
  }
  1. Use normalized command in handleBash():
  // Strip env var prefixes before pattern matching to prevent bypass
  // e.g., "LANG=C rm -rf /" should still match "rm -rf" patterns
  const normalizedCommand = stripEnvPrefix(command);
  const result = validateBashCommand(normalizedCommand);

The original command is preserved for logging — normalization is only applied to pattern matching.

Testing

Verified with 8 test cases:

Input Normalized Status
LANG=C some-cmd --flag some-cmd --flag Pass
A=1 B=2 another-cmd another-cmd Pass
ENV_VAR=value git status git status Pass
echo hello (no prefix) echo hello Pass
PATH=/usr/bin ls ls Pass
FOO="bar baz" cmd cmd Pass
SPACED=val cmd cmd Pass
NO_PREFIX_HERE NO_PREFIX_HERE Pass

Context

Claude Code v2.1.38 changelog entry:
Fixed bash permission matching for commands using environment variable wrappers

This fix was applied at the Claude Code platform level (built-in permission matching in settings.json). The SecurityValidator hook performs its own independent pattern matching and needs
the same fix applied separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions