Understand the security status of GitHub Actions workflows and how to mitigate the risk.
We recently published a report, called The State of GitHub Actions Security, which analyzes the security posture of GitHub Actions workflows and custom GitHub Actions. This report is based on an analysis of 2,500,000 GitHub Actions workflow files belonging to 553,000 organizations and personal users.
Some notable findings include the insecurity of the building blocks of GitHub Actions workflows.
All GitHub Actions automations are handled via workflows. The building blocks of the workflows include:
- Triggers: control when the workflow should run
- Jobs: a collection of steps to execute with a shared context
- Steps: the most basic building block -- an execution
- Runners: where the job should run (on which machine)
- Permissions: what permissions the workflow has
Security of Triggers
Triggers control when a workflow should run. GitHub Actions provide 36 event types that trigger workflows.
The most used triggers are shown in the table below.
Risky triggers
Some triggers inherently present more risk than others, for instance, pull_request_target and workflow_run. We found thousands of occurrences of both these triggers, listed in the table below.
Although useful, pull_request_target in particular opens the door to a wide range of security issues, including RCE.
This workflow checks out the pull_request code and then builds it (in a privileged context). Since the code is from an untrusted source (fork), it leads to RCE in a privileged context.
We found 2,341 pull_request_target workflows vulnerable to attacks like remote code execution.
In this example of workflow_run:
The workflow runs when ‘Run Tests’ workflow completes.
A ‘workflow_run’ triggered workflow acts as a triggered workflow that is executed when another workflow completes, in a privileged context (has access to secrets and tokens). Normally, it is used to chain workflows. In public repositories, it is used to process untrusted pull requests from forks. The fork pull request executes the “dangerous parts” and then triggers the workflow_run workflow to finish the processing.
We found 1,561 workflow_run workflows vulnerable to attacks like remote code execution.
Recommendation
When possible, avoid using dangerous triggers. If you can’t avoid them, use the following mitigation strategies:
- Don’t run the workflow automatically. Require a maintainer approval before executing (using labels or environments).
- Set the workflow token permissions to the bare minimum.
- Do not trust any input from the originating workflow; treat any data as potentially malicious.
Security of Jobs and Steps
Jobs and steps are the building blocks of a GitHub Actions workflow. A job is composed of several steps, while steps perform the actual work.
This build job is composed of two steps, one that downloads the repository source code and another that performs the actual build.
Steps can reference prebuilt workflows from third-party entities, such as open source builders and vendors. When a step uses the “uses: repository_name@ref” clause, the Actions engine will download ‘repository_name’ at the specified ‘ref’ and execute it.
There a several options to reference an Action:
Reference to a commit
Pro: The most secure way to reference a third-party Action, it guarantees the third-party Action version specified.
Con: The Actions are not updated automatically.
Reference to a tag
Pro: Using a published release of the Action, it receives updates only if the workflow author intends it to.
Con: The Action can change without the dependent workflow’s knowledge.
(For more explanation about the risks of mutable tags, see What Are Immutable Tags And Can They Protect You From Supply Chain Attacks?)
Reference to the main branch
This is insecure, as the workflow will use any code from the third-party Action without control.
Reference to a non-default branch
This is the most insecure way to reference a third-party Action, and imposes the same risk as “reference to the main branch,” with the additional risk that unlike the main branch, other branches usually don’t have security guardrails that validate their quality, such as code review and code scanning.
Reference to a docker image
A less common way to use custom Actions is to use prebuilt docker images. This method is not prevalent in the GitHub Actions ecosystem and imposes risk on the image consumers because they can’t access the third-party Action source code (without reverse engineering the image).
Conclusion
98.4 percent of the references used by jobs and steps are not following the best practice of dependency pinning, which specifies which package or library an Action can rely on.
Dependency pinning in GitHub Actions (and in general) is crucial for ensuring workflows are stable and reproduceable. By pinning dependencies, you specify the exact versions of the external packages or libraries your Actions rely on. This practice guards against unexpected changes or updates that could potentially introduce breaking changes, compatibility issues, or security vulnerabilities.
Security of Runners
The GitHub Actions runner is the engine that executes the workflow definition. It both communicates with the GitHub server to obtain credentials and configurations and reads the user workflow file and executes it according to the definitions. The runner code is open-source and hosted on GitHub https://github.com/actions/runner.
There are two ways to use runners:
- Managed runners - GitHub owned virtual machines that anyone can use instantly.
- Hosted runners – Self-hosted runner deployed by the user to meet individual requirements such as security, performance, or memory.
Self-hosted runners pose significant security risks (see blog on self-hosted runner security here), especially if untrusted workflows run on them. Malicious programs may run on the machine, and it's possible for the runner sandbox to be escaped, exposing access to the machine's network environment. Additionally, unwanted or dangerous data can be persisted on the machine.
This is especially critical for public repositories since they may allow the organization’s users to execute workflows on the runners used by the public repositories.
We found 44,803 public repositories using hosted runners.
Recommendations on using runners securely
We recommend only using self-hosted runners with private repositories. Forks of your public repository could potentially run dangerous code on your self-hosted runner machine by creating a pull request that executes the code in a workflow. GitHub-hosted runners, on the other hand, are clean, isolated virtual machines that are destroyed at the end of the job execution and do not pose the same risks.
Security of Workflow Permissions
Each workflow run is assigned a GitHub personal access token scoped to the current repository that enables it to communicate with a GitHub API. By default, this token is highly privileged and has access to the following scopes and corresponding APIs:
If a workflow is compromised, the token can be used to perform malicious actions and even take over the repository.
Therefore, it is important that workflow authors use the “permission” key to specify the scope their workflow requires.
Legit found that only 14 percent of all workflows limit token permissions; without these limits, the token could be used to perform malicious actions or take over the repository.
Learn More
Download the full report to get more details on GitHub Actions security, including:
- Vulnerabilities found in GitHub Actions workflows
- Risk mitigation for GitHub Actions workflows
- Security of custom GitHub actions
- Recommendations for using GitHub Actions securely