Safeguard
Developer Tools•

What is script injection in GitHub Actions and how do you stop it?

Script injection runs attacker text as shell code in a workflow. See how a pull request title becomes a command and how an environment variable fixes it.

Safeguard Security Team
3 min read

Script injection in GitHub Actions happens when attacker-controlled text, such as a pull request title or branch name, is substituted into a run step and executed as shell code. Stop it by passing the value through an intermediate environment variable instead of writing the expression into the script.

How it works

When a workflow step uses an expression inside a run block, GitHub evaluates the expression and pastes the result into a temporary shell script before the script runs. If the pasted value contains shell syntax, the shell treats it as commands.

GitHub's documentation gives this example. The step reads a pull request title and checks its prefix:

- name: Check PR title
  run: |
    title="${{ github.event.pull_request.title }}"
    if [[ $title =~ ^octocat ]]; then
      echo "PR title starts with 'octocat'"
    fi

A pull request whose title is a"; ls $GITHUB_WORKSPACE" closes the quoted string and runs the ls command on the runner. A real attacker would run something more harmful, and the runner may hold secrets.

Which inputs count as untrusted

GitHub says contexts that end with words such as body, default_branch, email, head_ref, label, message, name, page_name, ref and title should be treated as potentially untrusted. Examples include the issue title and the pull request body. Less obvious sources include branch names and email addresses, because they allow flexible content. GitHub notes that a branch name can itself contain quote characters, semicolons and shell commands and still be valid.

The safer pattern

Set the value as an environment variable on the step, then read the variable in the script:

- name: Check PR title
  env:
    TITLE: ${{ github.event.pull_request.title }}
  run: |
    if [[ "$TITLE" =~ ^octocat ]]; then
      echo "PR title starts with 'octocat'"
    fi

Now the value is data held in a variable, not text spliced into the script. GitHub calls this the preferred approach for inline scripts. Passing the value to an action as an argument is another safe route, because the context value is not used to generate a shell script.

A review checklist

  1. Search workflows for expressions inside run blocks that use title, body, ref, head_ref, message or email values.
  2. Move each one to an env entry and quote the variable in the script.
  3. Cap the permissions of the pipeline token, so an injected command can do less.
  4. Run CodeQL's GitHub Actions scanning, which GitHub says can detect potentially vulnerable workflows.
  5. Treat changes to workflow files as high-risk in code review.

Why this is a supply chain issue

A workflow that publishes packages or deploys software holds credentials. Injected code in that job can read secrets or push a poisoned release. The attacker needs only the ability to open an issue or pull request, not access to your accounts.

Where Safeguard fits

Safeguard's policies and gates can add a check at the pull request stage, and its SBOM and dependency scanning covers what the pipeline builds. Workflow hygiene still needs review by your team.

Quick check

Does quoting the expression fix it?

No. The expression is replaced before the shell runs, so a value can include its own quote and break out. Use an environment variable.

Are private repositories safe from this?

Less exposed, but anyone who can open a pull request or issue can supply the text. Fix it regardless.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.