Github: Widespread injection vulnerabilities in Actions
Github Actions supports a feature called workflow commands
(https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions) as a
communication channel between the Action runner and the executed action. Workflow commands are
implemented in runner/src/Runner.Worker/ActionCommandManager.cs
(https://github.com/actions/runner/blob/0921af735a3c8fb6cf22ddc8a868b742816e24cf/src/Runner.Worker/ActionCommandManager.cs)
and work by parsing STDOUT of all executed actions looking for one of two
command markers.
V2 commands have to start at the beginning of a line and look like this \u"::workflow-command
parameter1={data},parameter2={data}::{command value}\u". V1 commands can also start in the middle of
a line and have the following syntax: \u"##[command parameter1=data;]command-value\u". The current
version of the Github action runner supports a small number of different commands but the most
interesting one from a security perspective is \u"set-env\u". As the name suggests, \u"set-env\u" can be
used to define arbitrary environment variables as part of a workflow step. A simple example (in V1
syntax) would be ##[set-env name=VERSION;]alpha, which puts VERSION=alpha in the environment of all
succeeding steps in a workflow.
The big problem with this feature is that it is highly vulnerable to injection attacks. As the
runner process parses every line printed to STDOUT looking for workflow commands, every Github
action that prints untrusted content as part of its execution is vulnerable. In most cases, the
ability to set arbitrary environment variables results in remote code execution as soon as another
workflow is executed.
I've spent some time looking at popular Github repositories and almost any project with somewhat
complex Github actions is vulnerable to this bug class. A couple of examples to show how this bug
can be exploited in practice:
VSCode and CopyCat:
VSCode has a workflow for newly opened issues which runs
https://github.com/microsoft/vscode-github-triage-actions/blob/master/copycat/CopyCat.ts to copy
new issues into other repositories. As CopyCat prints the untrusted issue.title to stdout, it is
vulnerable to a workflow command injection.
Exploiting this instance is as easy as opening a new issue with the title \u"##[set-env
name=NODE_OPTIONS;]--experimental-modules
--experimental-loader=data:text/javascript,console.log(Buffer.from(JSON.stringify(process.env)).toSt
ring('hex'));//\u"
This will set the environment variable NODE_OPTIONS to the string \u"--experimental-modules
--experimental-loader=data:text/javascript,console.log(Buffer.from(JSON.stringify(process.env)).toSt
ring('hex'));//\u" which will get parsed by the Node interpreter during later execution steps. My
payload simply dumps the process environment in hex-encoded form to bypass secret redaction, but of
course more complex payloads are possible.
actions/stale:
Even Githubs own actions are vulnerable to this issue. actions/stale dumps untrusted issue titles
to STDOUT using core.info https://github.com/actions/stale/blob/ade4f65ff5df7d690fad2b171eeb852f4809dc0b/src/IssueProcessor.ts#L116, which boils down to a direct write to process.stdout
(https://github.com/actions/toolkit/blob/1cc56db0ff126f4d65aeb83798852e02a2c180c3/packages/core/src/core.ts#L153).
Fortunately, stale is often used as part of a single step workflow and I wasn't able
to exploit this bug class without executing a step after the workflow command injection. However,
workflows that use actions/stale and have multiple steps can be exploited in the same way as the
CopyCat issue from above (one example would be
https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/workflows/stale.yml)
Non-forked pull requests:
Actions that operate on issues are the most obvious attack vector, but non-forked pull requests are
also interesting. Actions triggered by forked pull requests don't have access to write tokens but
an external contributor can just create a pull request between two existing branches in the target
repo to trigger a privileged workflow run.
One interesting example for a vulnerable action is https://github.com/cirrus-actions/rebase which
triggers on /rebase comments on a pull request and which is used by a number of popular Github
repos such as https://github.com/ReactiveX/rxjs.
If the action is executed on a pull request that can't be rebased, the full Github API
representation of the PR is printed to STDOUT:
https://github.com/cirrus-actions/rebase/blob/b58fa7bac6d1d885234db03877f1d599c3ff48c7/entrypoint.sh#L48
This also includes the attacker controlled PR body and title and can be exploited by creating a
non-forked and non-rebasable PR with the following body: ##[set-env
name=NODE_OPTIONS;]--experimental-modules
--experimental-loader=data:text/javascript,console.log(Buffer.from(JSON.stringify(process.env)).toSt
ring('hex'));//\"
(This works even though the body is printed as part of a JSON document as V1 workflow commands can
start in the middle of a line.). Code execution is triggered by just commenting \u"/rebase\u" under the
PR.
As rebase relies on actions/checkout, this even works if rebase is the last step in a workflow.
Current versions of action/checkout define a post-execution step to cleanup git credentials and
which will trigger our exploit.
Suggested Fix:
I'm really not sure about the best way to address this issue. I think the way workflow commands are
implemented is fundamentally insecure. Deprecating the v1 command syntax and hardening set-env with
an allowlist would probably work against the direct RCE vectors. However, even the ability to
overwrite \u"normal\u" environment variables used by later steps is probably enough to exploit most
complex actions. I also did not look into the security impact of other workspace commands.
A good long-term fix would be to move workflow commands into some out-of-bound channel (e.g a new
file descriptor) to avoid parsing STDOUT, but this will break a lot of existing action code. (I do
not think that this should be addressed by simply patching vulnerable actions. Depending on the
programming language used it is pretty much impossible to guarantee that no malicious data will end
up on STDOUT. )
Proof-of-Concept:
My private repo github.com/felixwilhelm/actions includes vulnerable actions and triggers. Please
let me know if I should give someone on your side access to it.
Credits:
Felix Wilhelm of Google Project Zero
This bug is subject to a 90 day disclosure deadline. After 90 days elapse, the bug report will
become visible to the public. The scheduled disclosure date is 2020-10-19. Disclosure at an earlier
date is also possible if agreed upon by all parties.
Related CVE Numbers: CVE-2020-15228,CVE-2020-15228.
Found by: Felix Wilhelm