woodpecker-ci / woodpecker-ci/plugin-git

[Bug] `PLUGIN_SSH_KEY` allows command injection through `core.sshCommand`

Open
#385 0 comments 0 reactions 0 assignees View on GitHub

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
    constructs ssh -i plus the untrusted value and stores it as
    core.sshCommand.
  • plugin.go, fetch
    invokes the later Git fetch.
  • flags.go
    describes PLUGIN_SSH_KEY as 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 mentions core.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

attachments.zip

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.