GitHub Actions workflows are powerful because they can build applications, run tests, publish packages, deploy infrastructure, and respond to repository events. That flexibility also means a workflow can become a security boundary.
A small mistake in a workflow can allow attacker-controlled input to influence a shell command, file path, script, or downstream action.
One important class of problems is workflow injection: untrusted data from a GitHub event is incorporated into a command or other executable context without appropriate handling.
CodeQL can help identify these patterns during code scanning. But enabling a security query is only the beginning. Teams should also test whether their workflows contain detectable vulnerable patterns, whether legitimate patterns produce noise, and whether remediation actually removes the underlying risk.
This makes workflow-injection detection a good candidate for a security regression test.
What Is GitHub Actions Workflow Injection?
Consider a workflow that uses pull-request metadata:
name: Pull Request Check
on:
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Print title
run: echo "${{ github.event.pull_request.title }}"
The problem is not that pull-request titles are inherently dangerous.
The problem is that event data can be controlled by someone who is not trusted in the same way as repository-maintained workflow code. If that value is placed into a shell command, it can potentially be interpreted by the shell rather than treated purely as data.
The security model is therefore:
External Input
|
v
GitHub Event
|
v
Workflow Expression
|
v
Shell / Command
|
v
Execution
The dangerous transition is from untrusted data to executable syntax.
Why Pull Requests Need Special Attention
Pull requests are a common source of attacker-controlled values.
Examples include:
Pull-request titles
Branch names
Commit messages
Issue titles
Issue comments
Labels
Usernames
File names
Repository metadata
A workflow might use one of these values for:
Shell arguments
File paths
Environment variables
Command-line parameters
Generated scripts
Configuration
Not every use is vulnerable. The important question is whether untrusted data can alter the meaning of the command or operation.
A Vulnerable Pattern
Consider:
- name: Validate branch
run: |
echo "Checking ${{ github.head_ref }}"
The value is inserted directly into the shell script.
A safer pattern is to pass the value through an environment variable:
- name: Validate branch
env:
HEAD_REF: ${{ github.head_ref }}
run: |
echo "Checking $HEAD_REF"
This separation makes the shell receive the value as data rather than embedding it into the script source.
However, environment variables do not automatically make every command safe. If the variable is later passed to another command that interprets it as code or options, additional validation may still be necessary.
Why CodeQL Is Useful Here
CodeQL analyzes source and configuration code using queries designed to identify security-relevant patterns.
For GitHub Actions, security analysis can help identify relationships between:
Source
|
v
Untrusted Workflow Input
|
v
Data Flow
|
v
Sensitive Sink
A useful security analysis therefore goes beyond searching for a particular string.
It attempts to understand how data moves through the workflow.
Build a Controlled Security Test
Before evaluating detection, create a small test repository containing deliberately vulnerable workflow patterns.
For example:
security-tests/
|
+-- unsafe-title.yml
+-- unsafe-branch.yml
+-- unsafe-comment.yml
+-- safe-environment.yml
+-- safe-validation.yml
The repository should contain both vulnerable and intentionally safe examples.
This allows the evaluation to measure:
True positives
False positives
False negatives
without relying solely on production incidents.
Define Ground Truth First
Before running CodeQL, classify every test case.
For example:
| Test Case | Expected Classification |
|---|
| Untrusted title directly in shell | Vulnerable |
| Untrusted branch directly in shell | Vulnerable |
| Untrusted value passed through environment variable | Potentially safer |
| Constant shell command | Safe |
| Validated input passed to command | Depends on validation |
The exact classification should be reviewed by someone familiar with GitHub Actions security.
Do not define ground truth based on what CodeQL reports. That would make the evaluation circular.
Test More Than One Injection Pattern
A useful security regression suite should contain different source and sink combinations.
For example:
Event Input
|
+-- Pull Request Title
+-- Branch Name
+-- Issue Comment
+-- Commit Message
|
v
Potential Sink
|
+-- Shell Command
+-- Script
+-- File Operation
+-- External Tool
The goal is to determine whether the detection logic catches the security relationship rather than one exact syntax pattern.
Example: Pull-Request Title
Consider:
- name: Process title
run: |
./tools/process.sh "${{ github.event.pull_request.title }}"
Even though the value appears inside quotes, quoting alone should not be treated as a universal security guarantee.
The shell's parsing behavior and the surrounding command still matter.
A safer architecture is to keep event data separate:
- name: Process title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
./tools/process.sh "$PR_TITLE"
The called script should also treat the argument as data and validate it according to its requirements.
Example: Branch Names
A workflow may use a branch name to determine a deployment or environment:
- name: Deploy
run: |
./deploy.sh ${{ github.head_ref }}
This creates a direct connection between externally influenced data and command execution.
Instead, establish explicit mappings:
Branch
|
v
Validation / Allowlist
|
+-- main -> production
+-- staging -> staging
+-- other -> reject
For security-sensitive decisions, an allowlist is generally easier to reason about than accepting arbitrary branch strings.
Example: Issue Comments
Issue comments can also contain attacker-controlled text.
A workflow that executes a command based on comment content needs careful validation.
For example:
- name: Execute command
run: |
./admin-tool ${{ github.event.comment.body }}
The problem is larger than shell escaping.
The workflow is effectively allowing an external user to influence what the administrative tool receives.
A safer design is to parse a constrained command language:
Allowed commands:
/build
/test
/status
Everything else is rejected.
This is safer than passing arbitrary comment text to a shell.
CodeQL Detection Should Be Tested as a Pipeline
A useful evaluation looks like:
Test Repository
|
v
CodeQL Analysis
|
v
Security Findings
|
v
Ground-Truth Comparison
|
+---- True Positive
+---- False Positive
+---- False Negative
|
v
Detection Metrics
This turns a security scanner into something that can be measured.
Measure Precision and Recall
Two useful metrics are precision and recall.
Precision
Precision measures how many reported findings are actually valid:
Precision =
True Positives /
(True Positives + False Positives)
If CodeQL reports 20 findings and 18 are valid:
Precision = 18 / 20
Recall
Recall measures how many known vulnerabilities were detected:
Recall =
True Positives /
(True Positives + False Negatives)
If the test suite contains 20 known vulnerabilities and CodeQL identifies 18:
Recall = 18 / 20
The values above are examples, not benchmark results.
Your actual results should come from your test repository and configuration.
False Positives Matter
A security scanner that reports everything as suspicious may technically identify many dangerous patterns but become difficult for developers to use.
For example:
Finding:
Untrusted input reaches command.
Developer review:
Input is constrained by an allowlist.
That finding may require investigation rather than being an automatically confirmed vulnerability.
Track these cases separately.
A useful classification is:
Confirmed vulnerability
Likely vulnerability
Safe with justification
False positive
Needs manual review
False Negatives Are More Serious
A false negative occurs when a known vulnerable workflow is not detected.
For security tooling, these cases deserve particular attention.
Suppose the test suite contains:
10 vulnerable workflows
and only:
7 detected
The three missed cases should be examined individually.
Questions to ask include:
Did the source type differ?
Was the sink different?
Was data passed through another variable?
Did a workflow expression obscure the data flow?
Did the detection rule cover the relevant construct?
This analysis is more useful than simply reporting a percentage.
Include Safe Examples
A security regression suite should not contain only vulnerable workflows.
Add examples such as:
- name: Run tests
run: dotnet test
and:
- name: Display branch
env:
BRANCH_NAME: ${{ github.head_ref }}
run: |
printf '%s\n' "$BRANCH_NAME"
The objective is to determine whether the scanner can distinguish suspicious patterns from legitimate usage.
Test Remediation
Detection testing should include a second phase.
Start with:
Vulnerable Workflow
|
v
CodeQL Finding
|
v
Developer Fix
|
v
CodeQL Re-run
|
v
Finding Resolved
This verifies that the remediation actually removes the security condition.
Simply suppressing a finding does not demonstrate that the vulnerability was fixed.
Security Regression Tests in CI
Once the test cases are established, run them automatically.
A simplified pipeline can look like:
Pull Request
|
v
Build
|
v
Unit Tests
|
v
CodeQL
|
v
Security Regression Tests
|
v
Review
The test suite should remain small enough to execute consistently.
The purpose is not to recreate every production workflow in a security-test repository. It is to preserve known security patterns and ensure detection behavior remains useful.
Common Mistakes
Searching Only for ${{ github.* }}
Not every dangerous workflow uses the same expression.
Security analysis should consider data flow and the eventual execution context.
Assuming Quotes Make a Command Safe
Shell quoting is important, but it is not a substitute for understanding how data is consumed.
Testing Only Pull-Request Titles
Other event-controlled values can create similar risks.
Testing Only Vulnerable Examples
Without safe examples, false-positive behavior remains unknown.
Treating a CodeQL Finding as Proof
A scanner finding is an important security signal, but it still requires appropriate validation.
Suppressing Findings Without Fixing the Workflow
A suppressed finding can disappear from the report while the underlying vulnerability remains.
Troubleshooting CodeQL Detection
Expected Vulnerability Is Not Detected
First verify that the workflow is actually included in the analysis scope.
Then check:
CodeQL configuration.
Analysis language/configuration support.
Workflow syntax.
Data-flow path.
Query availability.
Whether the test pattern matches the intended security condition.
Avoid changing the test case immediately. First determine why it was missed.
Too Many Findings Appear
Review the reported flows.
Determine whether:
The input is genuinely attacker-controlled.
The sink is security-sensitive.
Validation occurs before the sink.
The finding is a legitimate false positive.
A Fixed Workflow Still Produces a Finding
Compare the old and new data flow.
For example:
Before:
Event -> Expression -> Shell
After:
Event -> Environment Variable -> Validated Argument -> Tool
If the finding remains, determine whether another unsafe path still exists.
Hardening GitHub Actions Workflows
Security scanning is only one layer.
Workflows should also follow broader security practices.
Minimize Token Permissions
Use the minimum GITHUB_TOKEN permissions required by each workflow.
For example:
permissions:
contents: read
Do not grant write permissions to a workflow that only needs to inspect repository contents.
Pin Important Actions
Where appropriate, pin third-party actions to reviewed versions or immutable references according to your organization's supply-chain policy.
Avoid Executing Untrusted Repository Code With Privileged Credentials
This is particularly important for workflows triggered by external contributions.
The security model of the workflow trigger should be reviewed carefully before secrets or write-capable tokens are exposed.
Separate Build and Deployment Trust Boundaries
A workflow that builds untrusted code should not automatically receive the same privileges as a trusted deployment workflow.
Best Practices
Treat workflow event data as untrusted input.
Keep untrusted data separate from shell source code.
Validate values before using them for security-sensitive operations.
Use allowlists for constrained commands and environments.
Minimize workflow token permissions.
Avoid exposing secrets to workflows that execute untrusted code.
Create controlled vulnerable and safe test cases.
Establish ground truth before scanning.
Measure both precision and recall.
Investigate false negatives carefully.
Test remediation, not just detection.
Run security regression tests continuously.
Review workflow changes as security-sensitive changes.
Do not treat a static-analysis tool as the only security control.
Advantages and Disadvantages
Advantages
Automated analysis can identify security-sensitive workflow patterns.
CodeQL can be incorporated into the existing GitHub security workflow.
Controlled test repositories make detection measurable.
Regression suites help preserve security coverage over time.
Developers can receive security feedback before deployment.
Disadvantages
Static analysis cannot guarantee that every workflow injection is detected.
False positives require developer investigation.
Workflow security depends on the trigger, permissions, secrets, and execution environment.
Complex data flows can be difficult to analyze.
A secure-looking workflow can still be dangerous if its surrounding permissions are excessive.
A Practical Security Testing Architecture
A mature workflow-security program can use multiple layers:
Pull Request
|
v
Workflow Configuration
|
+----------------+----------------+
| | |
v v v
CodeQL Security Tests Manual Review
| | |
+----------------+----------------+
|
v
Security Decision
|
+---------+---------+
| |
Pass Fix
| |
v v
Merge Re-run Analysis
The static-analysis layer identifies suspicious patterns.
The regression suite verifies known security cases.
Human review provides contextual judgment.
Together, these controls provide stronger coverage than any single mechanism.
Conclusion
GitHub Actions workflows should be treated as executable infrastructure, not merely configuration files. Event data can originate outside the repository's trusted codebase, and placing that data into shell commands or other executable contexts can create workflow-injection vulnerabilities.
CodeQL provides an important automated analysis layer, but teams should verify its effectiveness against controlled vulnerable and safe examples. Measuring true positives, false positives, and false negatives turns security scanning into an engineering process that can be evaluated and improved.
The strongest workflow-security strategy combines static analysis with least-privilege permissions, careful handling of untrusted inputs, controlled execution environments, and automated regression tests.
The key principle is simple: anything controlled by an external contributor should be treated as data until the workflow explicitly validates it as safe to use.