woodpecker-ci / woodpecker-ci/plugin-git
[Bug] `PLUGIN_SSH_KEY` allows command injection through `core.sshCommand`
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 25
- Forks
- 40
- Avg merge
- 22h 23m
- Merged PRs (30d)
- 6
Description
Summary
woodpeckerci/plugin-git v2.10.1 documents ssh-key as a private-key file
path. The implementation concatenates that setting into a Git
core.sshCommand string. Git later interprets that value as a shell command
when it launches SSH.
Shell metacharacters in a setting that is supposed to be a path therefore
execute commands in the clone plugin container. In the demonstrated path,
plugin-git has already written the Woodpecker netrc credential before Git
evaluates core.sshCommand, so the injected command can read that credential.
Affected source
The validation target is woodpeckerci/plugin-git v2.10.1, source commit
d88bfe670ece24c0150fab17d01399742b569ad9.
The source path is short and deterministic:
plugin.go,Plugin.Exec
writes the netrc and changes the command environment before building the Git
command sequence.plugin.go,sshKeyHandler
constructsssh -iplus the untrusted value and stores it as
core.sshCommand.plugin.go,fetch
invokes the later Git fetch.flags.go
describesPLUGIN_SSH_KEYas an SSH key file, not as a command fragment.- The public settings table has the same contract:
docs.md.
exec.Command safely passes the complete value to the first git config
process. That does not make it safe: the resulting Git configuration is a
command string and is parsed again by Git at the fetch boundary.
Reproduction
From the directory containing this report:
git clone https://github.com/woodpecker-ci/plugin-git.git attachments/vendor/plugin-git
git -C attachments/vendor/plugin-git checkout --detach d88bfe670ece24c0150fab17d01399742b569ad9
PLUGIN_GIT_DIR=attachments/vendor/plugin-git \
attachments/reproduce.sh attachments/replay
The script builds the pinned source and runs the plugin twice. The attack uses
an unreachable loopback SSH endpoint so no external service is required. The
control uses an ordinary key path. The synthetic credential is never sent to
the network and the output records only the expected marker value.
See attachments/evidence.md and
attachments/run.log.
Trigger
The attack value is structurally a path setting but contains shell syntax:
; cat <plugin-home>/.netrc > <fixture>/ssh-leak; #
With PLUGIN_USE_SSH=true, the plugin creates the workspace and writes the
netrc, configures core.sshCommand, and runs git fetch. The SSH connection
fails as expected; the injected cat has already run before the fetch result
is returned.
Expected behavior
PLUGIN_SSH_KEY should remain one path argument through the full execution
chain. Characters in the value must not be able to add shell commands to the
SSH invocation. Invalid paths should fail as paths, not be interpreted as a
program.
Observed behavior
The attack run returns the normal fetch failure but also reads the credential:
plugin_exit=128
private_home_netrc=present
injected_read=ci-secret
control_plugin_exit=128
control_marker=absent
The control has the same unreachable endpoint and the same expected non-zero
fetch result, but no injected marker.
Security impact and prerequisites
This is arbitrary command execution in the credentialed clone plugin
container. The injected command can read the netrc, modify the shared
workspace, or exfiltrate data over the plugin's network access. It does not by
itself demonstrate an escape from the plugin container or compromise of the
Woodpecker host.
The attack requires a caller who can influence the plugin setting and a
pipeline configuration that invokes the SSH clone path. The credential read
is demonstrated in the same process context in which plugin-git writes the
credential.
Related history and novelty
I did not find a report of shell command injection through PLUGIN_SSH_KEY or
this core.sshCommand construction. The closest records are different:
- plugin-git #231 and
PR #235 discuss
whether the setting contains key content or a key path; - Woodpecker #2496
discusses SSH configuration and mentionscore.sshCommand, but does not
identify command injection; and - Woodpecker #2601
discusses the broader risk of clone-plugin netrc exposure, but not this
shell-injection primitive.
This report is a new, source-realized command-injection path. It should be
reviewed together with the existing trusted-plugin/netrc model rather than
treated as a host-escape claim.
Suggested remediation
Do not concatenate an untrusted path into a shell command. Use a dedicated
SSH wrapper or a rigorously shell-quoted path at the Git boundary, and reject
values that are not valid paths before writing configuration. Add a regression
test that passes shell metacharacters in PLUGIN_SSH_KEY, confirms that the
fetch may fail, and still asserts that no side-effect command ran.
Attachments
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running attachments/reproduce.sh against the pinned source commit to observe the control and injection cases. Read Plugin.Exec, sshKeyHandler, fetch, flags.go, and docs.md to trace PLUGIN_SSH_KEY into core.sshCommand. Done means shell metacharacters remain a path value and a regression test confirms no side-effect command runs, even when fetch fails.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, go
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100