Safeguard
Security Guides•

How do I stop script injection in GitHub Actions run steps?

Never paste untrusted values like a pull request title into a run script. Pass them through an environment variable or an action instead. Before and after.

Safeguard Security Team
4 min read

Do not place expressions such as a pull request title directly inside a run script. Assign the value to an environment variable and read it as a normal shell variable, or pass it to an action as an input. This keeps attacker controlled text from being turned into shell commands.

Why the direct form is dangerous

In a workflow, an expression is replaced with its value before the shell sees the script. If that value comes from someone else, such as a pull request title, a branch name, an issue body or a commit message, the text becomes part of the script. A title like a"; ls $GITHUB_WORKSPACE" is the example GitHub uses to show how a quote and semicolon can end your command and start theirs.

Replacing the value first, and running the shell second, is what makes this class of bug. The fix is to change what the shell receives.

Before and after

Risky:

- name: Check title
  run: |
    echo "Title is ${{ github.event.pull_request.title }}"

Safer, using an intermediate environment variable, the approach GitHub calls preferred for inline scripts:

- name: Check title
  env:
    TITLE: ${{ github.event.pull_request.title }}
  run: |
    echo "Title is $TITLE"

GitHub explains why this works: the expression value is stored in memory and used as a variable, so it does not interact with script generation. In its example, the attempted injection fails, and the log simply shows the odd text as data. The docs also suggest double quoting shell variables to avoid word splitting, which is a general shell rule.

A second option from the docs: use an action instead of an inline script. A JavaScript action receives the context value as an argument, and is not vulnerable to this injection because the value is never used to generate a shell script.

Which values to treat as untrusted

Treat anything a contributor can influence as untrusted, even when the contributor is "internal":

  • Pull request title, body and branch names.
  • Issue titles and bodies, and comment text.
  • Commit messages and author names.
  • Anything from an artifact or a previous workflow run.

Values that your own configuration sets, like the repository name or a fixed string, are lower risk, but the habit of using the environment variable form for everything is simpler than deciding case by case.

A five minute audit

  1. Search workflow files for expressions used inside run: blocks.
  2. For each match, ask whether the value could be influenced by someone outside the team.
  3. Rewrite the match using an environment variable.
  4. Reduce token permissions on the job, so an injection that does land has less to reach.
  5. Turn on scanning for workflows. GitHub's docs say code scanning, including CodeQL, can detect potentially vulnerable workflows, and OpenSSF Scorecards can flag script injection and token permission issues.

Try it yourself

Static analysis catches this pattern in application code too. The demo shows a code scan finding an injection style flaw. Click a finding and read how the data flows from source to sink.

Click through it at your own pace, or open it full screen.

Where Safeguard fits

Safeguard offers code and dependency scanning, and policy gates for pull requests. For workflow files specifically, use the platform tools GitHub names above and review changes with a code owner. See protecting workflow files with CODEOWNERS.

Quick check

Is the environment variable form completely safe?

It stops the value changing the script. Your script must still handle the value safely, for example by quoting it and not passing it to eval or another interpreter.

Does this apply to GitLab CI?

Similar issues exist wherever user text is substituted into a script. The syntax differs, so check your platform's documentation.

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.