Writing GitHub Actions securely is notoriously difficult. While the default behavior is relatively secure, certain features may have non-obvious security implications.
The Apache Infrastructure GitHub Actions Policy has the formal rules around the use of GitHub actions. The content below is intended to be more practical advice.
IMPORTANT! You should enable CodeQL "actions" scanning in your repositories as described in https://github.blog/security/application-security/how-to-secure-your-github-actions-workflows-with-codeql/ - this will scan and flag those issues described below and many more automatically for you
Threat model
We're trying to protect:
- For projects that trigger releases from GitHub Actions: the signing key material
- For all projects: prevent an attacker from 'sneaking in' a commit to the main branch without review
- Any credentials for 3rd-party services that might be configured
We mainly focus on attacks that can be triggered by external attackers, though ideally compromised committer accounts should also be considered.
Workflow approval
By default, ASF repositories require approval before building PRs by non-committers.This has some limitations:
- This restriction does not apply to workflows triggered by
pull_request_target
orissue_comment
- This restriction does not protect against compromised committer accounts
- When using the 'checkout' action to check out the relevant PR, do not specify the
ref
as "refs/pull/${{ github.event.number }}/merge
" or "github.event.pull_request.head.ref
": the PR may have been updated since the workflow was approved. Instead, leave theref
empty to use the code associated with the event that triggered the build (which should what it looked like when it was approved), or use the exact commit hash.
Default GITHUB_TOKEN permissions
Each build will have access to a GITHUB_TOKEN to perform GitHub API calls. The permissions associated with this token depend on the trigger that started the build. You can find an overview here. Tokens for workflows triggered by pull_request
are safe, but others are not. For those:
- always make sure to configure minimal permissions and branch restrictions.
- if using the 'checkout' action, always enable
persist-credentials: false
Dangerous workflows
There are a number of really dangerous workflows in GitHub actions that you should only consider when it's absolutely necessary - and you should be extremely careful when deciding to use them: "pull_request_target" and "workflow_run" are particularly dangerous, also "issue_comment" should be used with care. There are multiple ways malicious users can use capabilities that those workflows give them, and new ways are being continuously discovered (abusing environment creation, using malicious PR titles and descriptions, poisoning the cache etc.). Best if you do not use those workflows at all.
There are several reasons why you might want to use the workflows - but most of them revolve around allowing "write" permissions in "secure way" for PRs coming from "forks". PRs from forks - by definition - cannot have any write permissions, and the separate workflows are supposed to bypass this limitation. One of the common problems is to be able to create and use "huge" cache between the runs, usually cache created in previous PR runs of the same un, or cache created by "target branch" runs. There is the https://github.com/apache/infrastructure-actions/tree/main/stash. action created by Jacob Wujciak that allows to use artifacts to store even huge cache (and store/restore the cache quickly and efficiently share the cache between branches). For example it allowed Apache Airflow to get rid of the "pull_request_target" workflow where they needed to share 2GB images between PR runs - providing a much simpler and easier to maintain workflow. See https://github.com/apache/airflow/pull/45266 for PR implementing this change.
If you think you cannot avoid dangerous workflows - it's best if you reach out to #builds slack channel on ASF slack - there is a group of peple there who discuss various ways you can design your Github Actions in the way to avoid dangerous workflows.
It's highly recommended to use https://woodruffw.github.io/zizmor/ static analysis tool in your CI / pipelines to detect and fix potential security issues in your workflows.
Builds triggered with pull_request_target
Builds triggered by the pull_request_target
event (as opposed to pull_request
) by default check out the 'target' branch of the PR, and run with a 'permissive' GITHUB_TOKEN. It is common for such workflows to switch to the PR that triggered the workflow. This is dangerous, as any code that is loaded from the repo and run after the switch (including actions, scripts, build tools and test code) may be untrusted, and will have access to the GITHUB_TOKEN.
Builds triggered with workflow_run
A common technique for building untrusted code but also using privileges to act on the build result is to split the build into two parts: a low-privilege one triggered by pull_request
that runs the untrusted code, stores the result in an artifact, and triggers a second, high-privilege build with workflow_run
that acts on that result.
In such a scenario, you must be careful to make sure all evaluation of untrusted code happens in the pull_request
build, and no untrusted code is executed in the workflow_run
part of the workflow.
When extracting and using the artifacts it is important to remember that they were produced in an untrusted context and their content is not to be trusted:
- Always extract into a directory separate from the trusted code in a step before checking out said trusted code. This stops files extracted from the archive from clobbering trusted code.
- A separate directory can also prevent files in the artifact impersonating often used python modules like
pip
as runningpython -m pip ...
would execute a pip.py file in the cwd - Validate any content you retrieve from an artifact before you use it to avoid command injection, especially in steps using
bash
and Github Actions macros.- This includes using
cat
on such files as well as putting their content into environment variables (a popular exploit is to modifyLD_PRELOAD
, some examples ), step outputs to use via${{ steps.id.outputs.sus_content }}
to be used in e.g.if
in bash.
- This includes using
Builds triggered with issue_comment
Similarly, builds triggered with issue_comment
run with a 'permissive' GITHUB_TOKEN. Again, no code may be loaded after switching to the PR branch. You use this technique to switch to the branch.
3rd-party actions
The Apache Infrastructure GitHub Actions Policy states actions outside of apache/*
, github/*
and actions/*
must be pinned to the specific git hash (SHA1) of the action that has been reviewed for use by the project. For instance, you MUST pin foobar/baz-action@8843d7f92416211de9ebb963ff4ce28125932878
.
Fixing workflows
When a security issue is fixed in a workflow triggered by pull_request_target
, you must not only fix it on your main branch, but fix/delete all branches that have the vulnerable workflow, as PRs to those branches would still trigger the vulnerable code.
Workflows triggered by workflow_run
only run when this workflow also exists on the main branch, so removing/renaming it on the main branch may also help there.
Further mitigations
- NEVER directly run code that might come with "forked" PRs in your workflows. There are certain exotic (but useful) workflows that are dangerous. For example, with "workflow_run" you might need to cancel duplicate workflows. Those workflows by default run with "master" code, but sometimes you might need to check out the incoming PR code for those. The host environment can have access (in various ways) to the "WRITE" GITHUB_TOKEN that has permission to modify your repository WITHOUT RESTRICTION OR NOTIFICATION. NEVER run the code that is checked out from the PR in your host environment. If you need to, run it in Docker Container to provide isolation from the host environment to avoid the "write" access leaking to users who prepare such a PR from their fork.
- NEVER install and run 3rd-party dependencies in the host of your build workflow code. Again there are ways those dependencies can obtain the "WRITE" GITHUB_TOKEN and change anything in your repository without your knowledge. There are very common "schedule" and "push" workflows that are especially prone to such abuse. Those run with "WRITE" access, and again there are ways to obtain the GitHub Token by these Actions and code that runs in your workflow. If you execute any 3rd-party code, run it in Docker containers to keep isolation from your "build" host environment to avoid leaking "write" access to those 3rd parties.
- If you REALLY HAVE TO run untrusted code (for example as part of your build steps) - you should only do it inside a docker container that you should not pass any credentials to.
- If you build images from Dockerfile that is untrusted make sure you use .dockerignore which is not coming from the PR, which Ignores everything from context by default. Add
**
as the first line and only adds (via!directory_path
or!file_path
) the files that you need during the build