Introduction
GitHub has recently made changes to GitHub Actions to improve security, and published a series of blog posts on how to secure Actions and Workflows (part 1, part 2, part 3). A recurring topic is the securing of GitHub Actions triggered by Pull Requests of untrusted authors. This is typically not a concern in a private repository, where only known and trusted collaborators are allowed to submit PRs. However, there are other security concerns that apply to both public and private repositories, and even unique security concerns that only apply to private repositories: to keep the codebase undisclosed.
In the April 2021 Codecov Bash Uploader incident, attackers compromised the uploader to exfiltrate credentials and code from repositories that used the Codecov Github Action or similar CI integrations. The attack remained unnoticed and unfixed for 2 months.
Besides third-party Github Actions, other attack vectors for GitHub Workflows also include dependency updates in the codebase. When using tools like Dependabot, updates for hijacked packages can result in automatically created PRs that execute malicious code during installation or testing in GitHub Workflows.
An example for this is the
2018 eslint-scope incident,
where an attacker gained access to the npm credentials and released package
updates with malicious postinstall
scripts. In this case, the update was
withdrawn in less than 3 hours.
In response to these growing risks, GitHub has recently made several
security-related changes to GitHub Actions. Since March,
Workflows initiated by Dependabot run with read-only permissions and no access to secrets.
One month later,
GitHub introduced a more fine-grained permission system for the GITHUB_TOKEN
.
In this article, we will discuss how to use this new functionality and other measures to reduce the attack surface of your private GitHub repositories with a focus on Node.js projects.
How to harden GitHub Workflows
Reduce permissions
It is now possible to restrict access to the repository for a GitHub Workflow by
changing the
permissions for the GITHUB_TOKEN
.
Keep in mind this does not affect permissions for custom Personal Access Tokens
(PAT).
Permission can be set at different levels — organization- or repository-wide by setting the global default permissions, for an entire Workflow or for an individual job inside a Workflow by specifying permissions inside a GitHub Workflow. Local permissions override the global defaults — full details on how the effective permissions are calculated can be found in the GitHub documentation.
By setting the global Workflow permissions to "Read repository contents permission", Workflows will only have read access to the codebase — all other permissions need to be set in the Workflows.
There are currently 12 permission scopes available inside the Workflows. Unfortunately, there is no documentation we know of explaining the scopes, so it may take some trial and error to get the right combination for each Workflow. For one of our favorite "publish" jobs (run semantic-release to determine the next version number and publish a release to the repository), we need write access to the repo content (tag/release), pull-requests (to update the PR comment after publishing) as well as deployments. The permissions look like this:
jobs:release:steps:- name: Semantic releaserun: npx semantic-releasepermissions:contents: writedeployments: writeissues: readpull-requests: write
Audit dependencies before installing
The GitHub Advisory Database collects information about known security vulnerabilities of packages for various ecosystems, including pip and Composer. Since early October, the npm advisory database has also been merged into GitHub's database.
npm's audit command can be used to scan project dependencies against the database. This can be done as part of a GitHub Workflow before installing the dependencies to abort the Workflow execution if vulnerable dependencies are found.
The audit output can be quite noisy at times, and not all vulnerabilities can be exploited in each project setup. To reduce the number of false positives that come up in the audit, it is possible to filter out vulnerabilities with low severity.
jobs:test:steps:- name: Audit Dependencies# This step will fail if at least one vulnerability with high severity# is foundrun: npm audit --audit-level=high
Avoid executing code during dependency installation
By default, packages installed with npm can execute arbitrary code by defining
install
or postinstall
scripts in their package.json
.
These scripts can be skipped by using the switch --ignore-scripts
. However,
using this switch will cause issues with packages that rely on these scripts to
perform legitimate installation tasks such as downloading or compiling
platform-specific binaries. In this case, you can manually run the scripts in
the Workflow for the packages that use them:
jobs:test:steps:- name: Install Dependenciesrun: npm ci --ignore-scripts- name: Install Sharpworking-directory: node_modules/sharprun: npm run install
The following one-liner can come in handy to find packages with (post)install scripts:
find ./node_modules/ -name 'package.json' | xargs grep -lE '"(post)?install":'
Limit exposure of secrets
Just like the the token permissions, env
variables with sensitive information
should not be exposed globally in the Workflow, but only for the steps that
actually need them.
jobs:test:env:# non-sensitive values are fine hereNODE_ENV: teststeps:- name: Run Testsrun: npm testenv:# the API key is only needed for this stepAPI_KEY: ${{ secrets.API_KEY }}
Going beyond
Once you hardened your GitHub Workflows as described above, you might ask yourself what else you can do to secure your codebase.
Here are two ideas:
- also consider dev environments: running
npm i
on a PR with compromised dependencies locally can expose the development machine — with potentially even worse implications than inside a GitHub Action - delay dependency updates: delaying the testing of updates by a few days
after their release, you increase the chance of compromised updates being
reported in the Advisory Database and picked up by
npm audit
If you have any questions or comments, please reach out on Twitter or start a discussion on GitHub.
Further reading
- Dependabot secrets
can only be used inside
dependabot.yml
, not in the Workflows - Suggestions for fixing GitHub Workflows
- Feature request for opt-in to allow secrets in Dependabot workflows