GitHub issue and PR comment injection: 83 hidden shell payloads hit workflows in 61 public repositories
Hidden shell commands in GitHub issues, PRs and comments hit 61 repos, including Trivy and MLflow. Find workflows that put issue text into run steps.
- Published
- Updated
- Ecosystem
- GitHub ActionsCI/CDOpen source
- Exploitation
- Yes. Radware saw the command execute in two workflow runs; theft of secrets was not confirmed
What happened
Researchers at Radware found at least 83 malicious GitHub issues, pull requests and comments across 61 public repositories. The count was current as of 13 October 2026 and the findings were reported on 29 October (The Register).
- 1 September 2026: 77 of the items were posted in a 43-minute span. Six more followed in the next two minutes.
- 74 GitHub accounts posted them. Many had real development histories, so they were likely compromised. The researchers suspect one person was behind it.
- Named targets include
agno-agi/agno,ToolJet/ToolJet,google/langextract,aquasecurity/trivy,CopilotKit/CopilotKit,QwenLM/qwen-code,mlflow/mlflow,discordjs/discord.js,cilium/cilium,brave/brave-browser,dotnet/maui,Homebrew/homebrew-cask,github/docs,apache/dorisandPX4/PX4-Autopilot.
All details above and below are from The Register unless stated. Radware’s own write-up is at radware.com; we could not open it, so we have no endpoint, account list or exact payload text to share.
How the attack works
- The attacker posts an issue, pull request or comment. Shell commands sit inside an HTML comment (
<!-- ... -->), so they do not show on the rendered page. - The event starts a workflow automatically, within about a second.
- If the workflow pastes the raw text into shell code, the shell runs the hidden command. The command gathered environment variables and tried to send them to an attacker server.
- Logs from two runs show the command executing. Radware could not confirm whether the attacker got write access, changed code or reached downstream users.
This is the script injection pattern GitHub documents. An expression such as ${{ github.event.pull_request.title }} is expanded before the script runs, so text that closes a quote becomes code. GitHub lists context properties ending in body, title, head_ref, label, message, name and others as untrusted input (GitHub Docs).
Am I affected?
Find workflows that react to issues and pull requests:
grep -rlnE '^\s*(issues|issue_comment|pull_request|pull_request_target|pull_request_review_comment|discussion|discussion_comment)\s*:' .github/workflows/In those files, find untrusted text expanded inside shell steps:
grep -rnE '\$\{\{\s*github\.event\.(issue|comment|pull_request|review|discussion)\.(title|body|head\.ref|head\.label)' .github/workflows/
grep -rnE '\$\{\{\s*github\.head_ref' .github/workflows/Any hit on a run: line, or in a script: block of actions/github-script, is injectable. A hit under env: is the safe form.
Then check whether someone tried it. List runs started by issue and comment events since 1 September:
gh run list --event issue_comment --created '>=2026-09-01' --limit 200
gh run list --event issues --created '>=2026-09-01' --limit 200
gh run list --event pull_request_target --created '>=2026-09-01' --limit 200And search issue and PR text for hidden HTML comments that contain commands:
gh api --paginate "repos/OWNER/REPO/issues/comments?since=2026-09-01T00:00:00Z" \
--jq '.[] | select(.body // "" | test("<!--[^>]*(curl|wget|bash|sh -c|\\$\\(|base64)")) | .html_url'
gh api --paginate "repos/OWNER/REPO/issues?state=all&since=2026-09-01T00:00:00Z" \
--jq '.[] | select(.body // "" | test("<!--[^>]*(curl|wget|bash|sh -c|\\$\\(|base64)")) | .html_url'The second call covers both issues and pull requests. A match plus a run of an injectable workflow at the same time means the command probably ran.
What to do now
- Fix the injectable steps. Move every
${{ github.event... }}value that carries user text out ofrun:and into anenv:entry, then use it as a quoted shell variable, for example"$ISSUE_BODY". GitHub calls this using an intermediate environment variable; the value is then handled as data, not inserted into the script (GitHub Docs: Secure use). - Rotate what a vulnerable job could read if your search found an injectable step and runs since 1 September. The payload collected environment variables, so that means every secret mapped into that job’s environment and the job’s
GITHUB_TOKENscope. - Split the jobs. Radware’s advice is to keep workflows triggered by outside events apart from jobs that hold elevated permissions, and to limit the secrets and variables those workflows can see (The Register). Set
permissions:to read-only on issue and comment bots. - Delete the bait. Edit or delete issues and comments that still carry hidden commands, so a later workflow change cannot trigger them again.
Why this matters
The payload is invisible on the page a maintainer reviews. Reading the issue tells you nothing. Only the raw body, or your workflow logs, shows the command. As one researcher put it, “every user with a GitHub account can try to run this attack.”