Context
A recent automated attack campaign (hackerbot-claw) targeted CI/CD pipelines across major open source repositories (Microsoft, DataDog, CNCF projects), achieving remote code execution in multiple targets and exfiltrating a GITHUB_TOKEN with write permissions from one repository with 140k+ stars.
I audited Evidently's GitHub Actions workflows against the specific attack vectors used in this campaign. No signs of compromise were found, but several hardening opportunities exist.
I'd be happy to open a PR for these fixes, but since some changes (particularly adopting Zizmor) would affect your CI/CD process, I thought it best to open an issue for discussion first.
Findings
1. Script Injection via ${{ }} in run: blocks (Medium-High)
Several workflows interpolate user-controlled values directly into shell commands, which enables script injection. An attacker can craft a branch name containing shell metacharacters to execute arbitrary commands.
Affected locations:
| File |
Lines |
Expression |
Notes |
main.yml |
172, 384, 441 |
${{ github.event.pull_request.head.ref }} in shell |
Branch name → shell injection |
main.yml |
274 |
${{ github.event.pull_request.head.ref }} as CLI arg |
Branch name → argument injection |
main.yml |
78, 84, 90, 96-97 |
${{ steps.changed-files.outputs.*_files }} in echo |
Filenames from PR → shell injection |
deploy-artifacts-to-github-pages.yml |
70 |
${{ github.event.workflow_run.head_branch }} in shell |
Most concerning — workflow_run runs with base repo's full write permissions and secrets |
docker.yml |
16 |
echo ${{ secrets.DOCKER_PWD }} |
Secret expanded inline in shell |
Fix: Assign ${{ }} values to environment variables, then reference them as "$VAR" in shell. Environment variables are treated as data, not code:
# Before (vulnerable):
run: |
BRANCH_NAME="${{ github.event.pull_request.head.ref }}"
# After (safe):
env:
BRANCH_NAME: ${{ github.event.pull_request.head.ref }}
run: |
BRANCH_NAME="${BRANCH_NAME//\//-}"
2. Missing Top-Level permissions: (Medium)
main.yml, docker.yml, and examples.yml don't set a top-level permissions: key, so the GITHUB_TOKEN gets the repository default (typically write-all).
Fix: Add permissions: read-all at the workflow level and grant specific write permissions only to jobs that need them. release.yml already does this correctly per-job.
3. Unpinned Actions (Medium)
All actions use mutable tag references (@v4, @v5) rather than immutable SHA pins. If a third-party action's repository is compromised, a force-pushed tag would run malicious code in your CI.
Notable concerns:
actions/checkout@master in docker.yml — tracks a branch, not even a tag
tj-actions/changed-files@v42 — this third-party action was the subject of a real supply chain attack in March 2025 where it was backdoored to exfiltrate CI secrets
Recommendation: Adopt Zizmor (docs) — a static analysis tool purpose-built for GitHub Actions security. It can:
- Detect all of the above issues (script injection, unpinned actions, permission problems)
- Run as a GitHub Action in CI to catch regressions
- Help generate SHA-pinned action references
You can also use Dependabot or Renovate to keep SHA-pinned actions up to date automatically.
4. Docker Secret Handling (Low)
docker.yml uses inline secret expansion for Docker login:
run: echo ${{ secrets.DOCKER_PWD }} | docker login -u ${{ secrets.DOCKER_LOGIN }} --password-stdin
release.yml already uses docker/login-action@v3 for this — the manual workflow should do the same.
Summary
| Issue |
Severity |
Effort |
Script injection in deploy-artifacts-to-github-pages.yml |
Medium-High |
Low (env var refactor) |
Script injection in main.yml |
Medium |
Low (env var refactor) |
Missing permissions: read-all |
Medium |
Low (add one line per workflow) |
Unpinned actions (especially tj-actions/changed-files) |
Medium |
Medium (adopt Zizmor + Dependabot) |
actions/checkout@master in docker.yml |
Medium |
Trivial |
Docker secret handling in docker.yml |
Low |
Low (use docker/login-action) |
Happy to submit a PR for some or all of these if that would be helpful — just let me know which changes you'd prefer to handle internally.
Context
A recent automated attack campaign (hackerbot-claw) targeted CI/CD pipelines across major open source repositories (Microsoft, DataDog, CNCF projects), achieving remote code execution in multiple targets and exfiltrating a
GITHUB_TOKENwith write permissions from one repository with 140k+ stars.I audited Evidently's GitHub Actions workflows against the specific attack vectors used in this campaign. No signs of compromise were found, but several hardening opportunities exist.
I'd be happy to open a PR for these fixes, but since some changes (particularly adopting Zizmor) would affect your CI/CD process, I thought it best to open an issue for discussion first.
Findings
1. Script Injection via
${{ }}inrun:blocks (Medium-High)Several workflows interpolate user-controlled values directly into shell commands, which enables script injection. An attacker can craft a branch name containing shell metacharacters to execute arbitrary commands.
Affected locations:
main.yml${{ github.event.pull_request.head.ref }}in shellmain.yml${{ github.event.pull_request.head.ref }}as CLI argmain.yml${{ steps.changed-files.outputs.*_files }}in echodeploy-artifacts-to-github-pages.yml${{ github.event.workflow_run.head_branch }}in shellworkflow_runruns with base repo's full write permissions and secretsdocker.ymlecho ${{ secrets.DOCKER_PWD }}Fix: Assign
${{ }}values to environment variables, then reference them as"$VAR"in shell. Environment variables are treated as data, not code:2. Missing Top-Level
permissions:(Medium)main.yml,docker.yml, andexamples.ymldon't set a top-levelpermissions:key, so theGITHUB_TOKENgets the repository default (typicallywrite-all).Fix: Add
permissions: read-allat the workflow level and grant specific write permissions only to jobs that need them.release.ymlalready does this correctly per-job.3. Unpinned Actions (Medium)
All actions use mutable tag references (
@v4,@v5) rather than immutable SHA pins. If a third-party action's repository is compromised, a force-pushed tag would run malicious code in your CI.Notable concerns:
actions/checkout@masterindocker.yml— tracks a branch, not even a tagtj-actions/changed-files@v42— this third-party action was the subject of a real supply chain attack in March 2025 where it was backdoored to exfiltrate CI secretsRecommendation: Adopt Zizmor (docs) — a static analysis tool purpose-built for GitHub Actions security. It can:
You can also use Dependabot or Renovate to keep SHA-pinned actions up to date automatically.
4. Docker Secret Handling (Low)
docker.ymluses inline secret expansion for Docker login:release.ymlalready usesdocker/login-action@v3for this — the manual workflow should do the same.Summary
deploy-artifacts-to-github-pages.ymlmain.ymlpermissions: read-alltj-actions/changed-files)actions/checkout@masterindocker.ymldocker.ymldocker/login-action)Happy to submit a PR for some or all of these if that would be helpful — just let me know which changes you'd prefer to handle internally.