GitHub Widespread Injection

Risk: Medium
Local: No
Remote: Yes

Github: Widespread injection vulnerabilities in Actions Github Actions supports a feature called workflow commands ( as a communication channel between the Action runner and the executed action. Workflow commands are implemented in runner/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 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, which boils down to a direct write to process.stdout ( 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 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 which triggers on /rebase comments on a pull request and which is used by a number of popular Github repos such as 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: 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 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


Vote for this issue:


Thanks for you vote!


Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.

(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2021,


Back to Top