Hey! I’m Shreya Pohekar, and I work as a Security Researcher at Microsoft. Over time, while working across CI/CD pipelines and reviewing developer workflows, I realized that one of the most overlooked aspects of GitHub Actions security is permissions. Developers often give Actions far more access than necessary, leaving room for privilege escalation and repository compromise.
In this blog, I’ll walk you through how GitHub permissions work, why least privilege matters, why pull_request is still safe even with write-all, and how you can structure your workflow permissions correctly using simple, real-world examples.
If you haven’t read my detailed breakdown of pull_request vs pull_request_target, you can check it out here:
How Attackers Exploit pull_request_target.
It will give you useful context for understanding how permissions interact with workflow triggers.
1. Why Permissions Matter in GitHub Actions
When a workflow runs, GitHub provides an authentication token (GITHUB_TOKEN).
This token has a set of permissions, such as:
contents: read/writepull-requests: writepackages: writeissues: writechecks: write
These permissions define exactly what that workflow can do.
If your workflow gets compromised, these permissions determine how much damage an attacker can cause.
For example:
- A token with
contents: writeallows modifying repo files. - A token with
issues: writeallows creating malicious issues or comments. - A token with
packages: writeallows pushing malicious packages.
So the goal is simple:
Give the workflow only the permissions it needs—nothing more.
2. Using Least Privilege (The Recommended Standard)
GitHub allows defining permissions globally or at the job level:
Global least-permission setup
permissions:
contents: read
pull-requests: read
Job-level permissions
jobs:
test:
permissions:
contents: read
By default, GitHub Actions used to grant broad write permissions.
Now, GitHub has moved toward a stricter model, but developers still often override permissions unconsciously.
Why least privilege is important
If your workflow gets compromised through:
- Dependency installation
- A malicious script
- A compromised Action
- An untrusted PR
…the attacker will only be able to use capabilities the token allows.
If your token has only read permissions:
- The attacker cannot modify the repo
- They cannot push new branches
- They cannot create releases
- They cannot comment or create issues
3. Why You Are Still Safe Even With write-all in a pull_request Trigger
This is the part many people misunderstand.
Even though GitHub allows you to configure:
permissions: write-all
…you are still safe under the pull_request trigger.
Here’s why:
1. GitHub automatically downgrades permissions for workflows triggered by untrusted PRs.
If someone from a fork opens a PR:
- The workflow runs in a read-only security sandbox.
- Secrets are not available.
GITHUB_TOKENis forced to read-only, even if you explicitly set it towrite-all.
This is why pull_request is considered the safe event.
Example
You write:
permissions: contents: write
on: pull_request
But a contributor opens a PR from a fork.
GitHub silently converts permissions to:
contents: read
Regardless of your config.
This is a built-in protection mechanism.
2. Workflow code is taken from the PR branch but executed with downgraded privileges
So even if the PR contains malicious workflow changes:
- It cannot push changes
- It cannot modify branches
- It cannot create tags
- It cannot deploy anything
This is why pull_request is safer than pull_request_target (explained in detail in the linked blog).
4. When Permissions Become Dangerous
The real trouble begins when combining:
pull_request_target- With high permissions
- With
actions/checkoutchecking out the attacker’s branch
This exposes your environment to attacks because:
pull_request_target runs in privileged context and does not downgrade permissions.
If your workflow contains:
on: pull_request_target
permissions: write-all
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: ./build.sh
You have essentially given:
- Write access
- Token permissions
- Ability to run arbitrary attacker code
This is why attackers target such workflows.
I have explained this attack path in detail here:
How Attackers Exploit pull_request_target.
5. Example: Safe vs Unsafe Permissions
Unsafe Example
on: pull_request_target
permissions: write-all
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install
- run: npm run build
Why unsafe?
- Untrusted code gets executed with write permissions.
- PR code can steal secrets or modify the repo.
Safe Example
on: pull_request
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
Why safe?
- The PR code runs in a downgraded environment.
- No secrets.
- Token is read-only.
- Cannot modify the repo even if compromised.
6. Best Practices for Handling Permissions Safely
1. Start with contents: read
In most cases, workflows need only read access.
2. Grant write permissions only to specific jobs
jobs:
release:
permissions:
contents: write
3. Never give write permissions in pull_request_target
If you truly need metadata updates, keep them read-only or strictly scoped.
4. Avoid using write-all unless absolutely necessary
Always restrict to the minimum.
5. Avoid running untrusted code in privileged workflows
This is the core principle behind all supply-chain security.
Here’s a concluding paragraph you can add to the permissions blog:
Managing permissions in GitHub Actions doesn’t need to be complicated, but it does require discipline. When you default to least privilege, audit what your workflows actually need, and understand how triggers like pull_request behave, you significantly reduce the chances of accidental exposure. Even if GitHub grants broader write permissions by default in certain contexts, the isolation guarantees of pull_request ensure your secrets remain protected, giving you a safer baseline to build on. Ultimately, good security in CI/CD is less about fear and more about thoughtful design—knowing what to trust, when, and why.
Hope you enjoyed reading this. See you in the next one. Until then, happy hunting.