GitHub Actions

GitHub Actions Script Injection: Safer Shell Steps

A shell step is safer when untrusted pull request text stays data, not syntax, from expression expansion through command execution.

Key takeaways for GitHub Actions script injection

  • GitHub recommends mitigating script injection in workflows and lists actions or intermediate environment variables as preferred approaches. [1]
  • GitHub's secure-use reference shows pull request title data as an example of untrusted input that should not be interpolated directly into inline shell logic. [1]
  • GitHub warns that some contexts should be treated as untrusted because an attacker could insert malicious content. [2]
  • The github.event context is the full webhook payload for the event that triggered the workflow run, so its shape and trust level vary by event. [2]
  • GitHub documents stop-commands for logging text without processing workflow commands and says the resume marker should be random and unique to the run. [3]

Script injection happens before the shell can protect you

Bottom line: Treat event fields, titles, labels, branch names, and issue text as untrusted; pass them through environment variables or actions instead of placing expressions inside shell scripts.

GitHub's secure-use reference explicitly calls out good practices for mitigating script injection attacks in workflows. It recommends using an action instead of an inline script, or using an intermediate environment variable when inline shell is needed. [1]

The risk is not limited to obvious user comments. Pull request titles, branch names, labels, issue bodies, and event payload fields can become dangerous when a workflow expands them into a run: block before the runner invokes Bash, PowerShell, or another shell.

Inline shell turns expression expansion into executable syntax

GitHub's script-injection guidance uses github.event.pull_request.title as an example input and then shows a safer pattern that assigns the expression to an environment variable before the shell logic reads it. [1]

That distinction matters because the Actions expression is resolved before the shell interprets the script. Once untrusted text is inserted into a command line, quoting mistakes, command separators, substitutions, and newlines can change what the shell executes.

Avoid this posture: A workflow checks a pull request title by placing ${{ github.event.pull_request.title }} directly inside a Bash conditional.

Contexts should be classified before use

GitHub says contexts can be used in places where default runner variables are unavailable, including expression processing before a job is routed to a runner. [2] That is useful for routing jobs, but it also means context data can influence workflow structure before a shell step starts.

GitHub also warns that workflow and action authors should consider whether their code might execute untrusted input from possible attackers, and that certain contexts should be treated as untrusted input. [2]

Review each context by source. Repository metadata, event payload fields, pull request data, issue content, and manual inputs do not all carry the same trust level, even when they are available through the same expression syntax.

Environment variables keep untrusted text as data

For inline scripts, GitHub describes an intermediate environment variable as the preferred approach for handling untrusted input. In the documented pattern, the expression is assigned under env:, then the shell reads that environment variable in its own quoting context. [1]

This does not make the value trusted. It makes the trust boundary visible. The shell receives a variable value rather than newly spliced script text, so reviewers can focus on validation, quoting, and the command that consumes the value.

Clean verdict: Untrusted event data enters the step through named environment variables, then shell code validates it before using it in commands.

Workflow commands create a second output channel to review

GitHub documents workflow commands as a way for actions and shell commands to communicate with the runner to set environment variables, output values, add debug messages, and perform other tasks. [3]

That means output can be more than output. If a step prints untrusted text that looks like a workflow command, review whether command processing should be stopped while the text is logged. GitHub documents stop-commands for logging anything without accidentally running a workflow command, and says the resume token should be randomly generated and unique for each run. [3]

Log review still matters after injection controls

GitHub recommends reviewing workflow logs after valid and invalid input tests because secrets may end up in error logs, and command output can send data to STDOUT or STDERR in ways that are not obvious. [1]

It also documents add-mask as a workflow command that prevents a string or variable from being printed in the log, while warning that the value should be registered before it is output or used in other workflow commands. [3]

A fast script-injection audit starts with every run block

Search workflow files for run: blocks that contain ${{. Classify each expression by source, then move untrusted event data into env: variables, validate expected formats, and keep shell quoting simple.

Next, inspect any step that prints event data, generated content, or tool output. Use workflow-command controls when command-looking text could be printed, and review logs from both expected and malformed inputs.

Run gha-guard locally so script-injection review sits beside pinned-action, token-permission, timeout, pull-request trigger, OIDC, reusable-workflow, runner, cache, attestation, environment, dependency-review, shell-context, and required-check audits.

This article is informational and is not a substitute for a security review of your own workflows, repositories, or cloud accounts.

CI Tripwire has not commissioned independent expert review of this article. Read more about the organization byline at contributors and the source posture at sourcing.

Corrections can be routed through the corrections note. Sources: 3 entries, primary GitHub documentation, last reviewed 2026-06-08.

Sources

  1. GitHub Docs, Secure use reference.
  2. GitHub Docs, Contexts reference.
  3. GitHub Docs, Workflow commands for GitHub Actions.