Skip to content

[Bug] PLUGIN_SSH_KEY allows command injection through core.sshCommand #385

Description

@N0zoM1z0

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions