Safeguard
Security Guides•

How do you prevent script injection in a security automation workflow?

Never place github context values such as pull request titles inside run scripts. Pass them via environment variables or an action. Examples and checklist.

Safeguard Security Team
4 min read

Do not write untrusted values such as a pull request title, branch name or issue body directly into a run script with the ${{ }} expression. Assign them to an environment variable and read the variable in the script, or pass them as an input to an action. This keeps attacker text out of the shell command.

Why automation is a target

A security workflow reads text that outsiders can control: pull request titles, branch names, commit messages, issue comments, advisory summaries and webhook payloads. It then often runs with write permissions or secrets. That is the exact setup an attacker wants.

GitHub's security hardening guide explains the mechanism. An expression in a run step is evaluated and substituted before the shell script is generated. If the value contains shell syntax, it becomes part of the script. A title written to close a quote and run another command is the classic example.

The unsafe and the safe form

Unsafe, with the context value inside the script:

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

Safe, with an intermediate environment variable:

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

GitHub's guide describes the second pattern: the expression value is stored in memory and used as a variable, so it does not interact with script generation. In its worked example the attempted injection fails and the log shows the malicious text as plain data. The guide also lists a stronger option, which is to use an action that takes the value as an argument instead of an inline script, and it recommends double quoting shell variables to avoid word splitting.

Checklist for automation workflows

  1. Search your workflow files for ${{ github.event inside run: blocks. Treat every hit as suspect.
  2. Move each value to env: and reference $NAME in the script.
  3. Quote variables: "$NAME".
  4. Never build a command from a webhook payload field. Validate against an expected pattern first, such as a package name regex.
  5. Do not echo untrusted text into the job summary or a chat message without escaping. See writing a report as a job summary.
  6. Keep write tokens and secrets away from jobs that process untrusted text. Split into two jobs: one reads and validates, one acts.

Context fields to treat as untrusted include titles, bodies, branch names, commit messages and author names, since all of them are chosen by the author of a contribution.

Privileged triggers need extra care

GitHub's guide singles out pull_request_target and workflow_run: when they are combined with checkout of an untrusted pull request, they are privileged and can expose the repository. It recommends avoiding pull_request_target if it is not necessary, and says workflow_run is a better trigger for privilege separation. See using pull_request_target safely.

Test it

Open a pull request whose title is a"; echo injected; " against a test repository and watch what your workflow does. If the log prints the string as data, you are fine. If a command runs, fix it.

Try it yourself

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

Look at how injection style findings are described in code, then apply the same eye to your workflow files.

Safeguard's pipeline scanning and policy gates can help you check changes before merge. Try the free scan at gold.safeguard.sh/scan.

Quick check

Is an environment variable always safe?

It removes the injection into script generation, but you still must quote it in the shell and avoid passing it to commands that interpret it, such as eval.

Which fields are untrusted?

Anything a contributor or outside sender controls: titles, bodies, branch names, commit messages and webhook payload fields.

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.