Skip to content

Commit aee67c2

Browse files
committed
Prevent ssh command injection
1 parent 8adb4fc commit aee67c2

3 files changed

Lines changed: 45 additions & 2 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Release 0.14.0 (unreleased)
1414
* Fix "unsafe symlink target" error for archives containing relative ``..`` symlinks (#1122)
1515
* Fix ``dfetch add`` crashing with a ``ValueError`` when the remote URL has a trailing slash (#1137)
1616
* Fix arbitrary file write via malicious tar/zip symlink (#0)
17+
* Prevent ssh command injection (#0)
1718

1819
Release 0.13.0 (released 2026-03-30)
1920
====================================

dfetch/vcs/git.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,27 @@
2929
logger = get_logger(__name__)
3030

3131

32+
_SHELL_METACHAR_RE = re.compile(r"[;&|`$(){}<>\n\r!]")
33+
34+
35+
def _sanitize_ssh_cmd(ssh_cmd: str, source: str) -> str | None:
36+
"""Return *ssh_cmd* if it is safe to use, otherwise log a warning and return None."""
37+
if _SHELL_METACHAR_RE.search(ssh_cmd):
38+
logger.warning(
39+
"Ignoring %s: contains unsafe shell characters, falling back to 'ssh'",
40+
source,
41+
)
42+
return None
43+
return ssh_cmd
44+
45+
3246
def _build_git_ssh_command() -> str:
3347
"""Returns a safe SSH command string for Git that enforces non-interactive mode.
3448
3549
Respects existing GIT_SSH_COMMAND and git core.sshCommand.
3650
"""
37-
ssh_cmd = os.environ.get("GIT_SSH_COMMAND")
51+
raw = os.environ.get("GIT_SSH_COMMAND")
52+
ssh_cmd = _sanitize_ssh_cmd(raw, "GIT_SSH_COMMAND") if raw else None
3853

3954
if not ssh_cmd:
4055

@@ -43,7 +58,10 @@ def _build_git_ssh_command() -> str:
4358
logger,
4459
["git", "config", "--get", "core.sshCommand"],
4560
)
46-
ssh_cmd = result.stdout.decode().strip()
61+
raw_config = result.stdout.decode().strip()
62+
ssh_cmd = (
63+
_sanitize_ssh_cmd(raw_config, "core.sshCommand") if raw_config else None
64+
)
4765

4866
except SubprocessCommandError:
4967
ssh_cmd = None

tests/test_git_vcs.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,30 @@ def test_ls_remote():
225225
None,
226226
"ssh -o BatchMode=yes",
227227
),
228+
(
229+
"injection via semicolon in env var",
230+
"ssh; rm -rf /",
231+
None,
232+
"ssh -o BatchMode=yes",
233+
),
234+
(
235+
"injection via pipe in env var",
236+
"ssh | evil",
237+
None,
238+
"ssh -o BatchMode=yes",
239+
),
240+
(
241+
"injection via subshell in git config",
242+
None,
243+
"$(evil_cmd)",
244+
"ssh -o BatchMode=yes",
245+
),
246+
(
247+
"injection via backtick in env var",
248+
"ssh `evil`",
249+
None,
250+
"ssh -o BatchMode=yes",
251+
),
228252
],
229253
)
230254
def test_build_git_ssh_command(name, env_ssh, git_config_ssh, expected):

0 commit comments

Comments
 (0)