Why GitHub Actions injections keep appearing

GitHub Actions workflow injections remain one of the most frequently encountered vulnerabilities in repositories hosted on GitHub. The root cause is straightforward: an attacker manages to get a command executed by one of your workflows. This typically happens when untrusted input—an issue title, a branch name, or other user-controlled data—is interpolated directly into a workflow step via the ${{ }} expression syntax.

During the preprocessing phase, GitHub Actions expands these expressions. If an attacker embeds shell metacharacters (like backticks) in the input, that expansion can inject new commands into the run block. When the workflow executes, the attacker's commands run with whatever permissions the workflow has been granted. The two biggest obstacles to fixing this class of bug are awareness that it exists and finding every instance where untrusted data flows into a workflow.

- name: print title
  run: echo "${{ github.event.issue.title }}"

Practical defenses for your workflows

While no approach is foolproof, a few habits will meaningfully reduce your exposure. These aren't one-time fixes—treat them as part of an ongoing security review process rather than a checklist you complete and forget.

Keep untrusted input out of run blocks

The most direct mitigation is to stop interpolating expressions directly into sections that execute shell commands. Instead of embedding the value in the command itself, expand it into an environment variable first, then reference that variable.

- name: print title
  env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "$TITLE"

This doesn't make the underlying data trustworthy, but it does block the most common injection techniques. The data remains untrusted—treat it accordingly.

Stick to least privilege for tokens

When an injection succeeds, the malicious commands execute with the permissions associated with the workflow's GITHUB_TOKEN. That makes token scoping an essential control. Define explicit permissions for each workflow so it only gets the minimum access required for its intended job. A narrow token limits what an attacker gains even if they manage to inject code.

Understand the pull_request_target risk

The choice between pull_request and pull_request_target triggers has significant security implications. The pull_request trigger restricts write permissions and secrets access for workflows triggered from forks—though not for branches in the same repository. The pull_request_target trigger, by contrast, was designed to let workflow authors relax those restrictions for legitimate use cases. That flexibility is exactly what makes it more dangerous. Reserve pull_request_target for situations where you have a concrete need, and audit those workflows more aggressively if you do use it.

Don't forget other branches

Security reviews often focus on the default branch, but if you use pull_request_target, vulnerabilities can live on any publicly visible branch. Attackers can target workflows associated with pull requests against feature branches just as easily as against main. Review all branches that are visible in your repository, not just the primary one.

Using CodeQL to find injection points

GitHub's CodeQL code analysis platform can automate much of the hunting. The ability to scan GitHub Actions workflow files is now generally available, and the code scanning feature detects potential workflow injection risks among other query types.

CodeQL's effectiveness here comes largely from taint tracking, which follows untrusted data as it flows through your codebase and flags risky endpoints that may not be obvious from a manual read. Enabling this for workflows requires no special configuration if you use default setup for code scanning—it analyzes workflow files automatically and runs against any protected branches. If you're on the advanced setup, you simply add actions to the list of languages CodeQL should scan. Going forward, those scans will include workflow files.

As with any automated security tool, CodeQL will not catch every issue. It's a strong complement to, not a replacement for, the security mindset described above. Keeping least-privilege, input-handling, and trigger-selection principles in mind while writing workflows remains your first line of defense against this prevalent and avoidable vulnerability class.