Yes. If a workflow puts a value like a pull request title directly into a run step, the text is substituted into the shell script before it runs, so a crafted title can execute commands on the runner. Pass the value through an environment variable instead.
How the injection works
GitHub's documentation explains that expressions inside the dollar and double brace syntax are evaluated and then substituted with their values. The result becomes part of the script. Consider this vulnerable step:
- name: Check PR title
run: |
title="${{ github.event.pull_request.title }}"
if [[ $title =~ ^octocat ]]; then
echo "ok"
else
echo "bad title"
exit 1
fi
The documentation shows that a title such as a"; ls $GITHUB_WORKSPACE" closes the quoted assignment early, so the attacker's command runs. In a workflow with secrets or write permissions, the command could be far worse than listing files.
Which values are risky
The docs list context values that should be treated as potentially untrusted, including those ending in body, head_ref, label, message, name, ref and title. Examples are the issue title and the pull request body. The warning also covers less obvious inputs such as branch names and email addresses.
| Source of input | Who controls it |
|---|---|
| Pull request title and body | Anyone who can open a pull request |
| Issue title and body | Anyone who can open an issue |
| Branch name (head_ref) | The branch creator, including forks |
| Commit message | The commit author |
The fix: an intermediate environment variable
GitHub's hardening guide recommends setting the value of the expression to an intermediate environment variable for inline scripts, so untrusted input does not alter the script. Its preferred approach is a JavaScript action that takes the value as an argument.
- name: Check PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ "$PR_TITLE" =~ ^octocat ]]; then
echo "ok"
else
exit 1
fi
The shell now reads the title as data from a variable, not as part of the program text. Always quote the variable.
Why this matters more for maintainers
Open source repositories accept input from strangers by design. Issue triage bots, labelers and title checkers all handle that input. Combine this risk with a privileged trigger and it gets worse. See pull_request_target risks for open source maintainers and least privilege GITHUB_TOKEN for a release workflow.
Audit steps
- Search workflows for the dollar brace pattern inside run blocks.
- For each, ask whether the value comes from a user.
- Move it into an env variable, or into an action input.
- Quote it in the shell.
- Add the workflows folder to CODEOWNERS.
Try it yourself
Click through it at your own pace, or open it full screen.
Click through a gated pull request and see which checks run before a merge. Workflow changes are exactly the kind of change to gate.
Safeguard can gate pull requests with policies; the free scan at gold.safeguard.sh/scan covers your dependencies.
Quick check
Is a branch name really attacker controlled?
Yes, for pull requests from forks the branch name is chosen by the contributor, and the docs list head_ref among risky values.
Is quoting the variable enough?
Quoting inside the shell helps, but the main fix is to keep the value out of the script text by using an env variable.
Sources
Last reviewed: 19 October 2026.