GitHub pull_request_target Workflow on Self-Hosted Runner


Description

Detects when a pull_request_target workflow runs on a self-hosted runner. pull_request_target workflows run with elevated privileges and have access to repository secrets even when triggered by external contributors from forks. When these workflows run on self-hosted runners attackers can gain direct code execution on the underlying infrastructure with potential access to internal network, databases, and systems. Unlike GitHub-hosted runners which are destroyed after each job, self-hosted runners persist and can be permanently compromised. This pattern is high risk regardless of whether the PR is cross-fork or same-repository because self-hosted runners represent infrastructure access. GitHub explicitly warns never to use self-hosted runners with public repositories or workflows that can be triggered by untrusted contributors. This configuration allows any GitHub user with read access to your repository to execute arbitrary code on your infrastructure.

Query

Detection:
  - Group:
      - ID: PullRequestTarget
        RuleID: GitHub.Webhook.PullRequestTargetUsage
      - ID: SelfHostedRunner
        RuleID: GitHub.Webhook.SelfHostedRunnerUsed
    MatchCriteria:
      field_name:
        - GroupID: PullRequestTarget
          Match: workflow_run.id
        - GroupID: SelfHostedRunner
          Match: workflow_job.run_id
    EventEvaluationOrder: Chronological
    LookbackWindowMinutes: 1800
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 10

Rule dependencies

Higher-order rule. It fires on other rules' alerts, not on raw events, so it cannot fire on its own. Deploy the rules it depends on too.

Depends on

Analyst notes

  1. Stop the self-hosted runner: - SSH/access the runner system - Stop the runner service or unregister the runner

  2. Isolate the runner from network: - Block outbound connections via firewall - Disconnect from internal network if possible

  3. Review the workflow: - Disable or delete the workflow file that uses pull_request_target + self-hosted - Or modify to use "runs-on: ubuntu-latest"

  4. Check runner system for compromise: - Review auth logs for unauthorized access - Check network connections: netstat -tunap | grep ESTABLISHED - Look for persistence mechanisms (cron, systemd, rc.local) - Check for suspicious processes or files - Review bash history for malicious commands

  5. Review recent workflow runs: - Check all workflows that ran on this runner in past 7 days - Look for other suspicious activity - Identify if compromise occurred in previous runs

  6. Search for indicators of compromise: - Outbound connections to suspicious IPs/domains - New user accounts or SSH keys on runner system - Modified system files or configurations - Cryptocurrency miners or backdoors - Data staging areas or compressed archives

Raw source GitHub pull_request_target Workflow on Self-Hosted Runner · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: correlation_rule
RuleID: "GitHub.PullRequestTarget.WITH.SelfHostedRunner"
DisplayName: "GitHub pull_request_target Workflow on Self-Hosted Runner"
Enabled: false
Severity: High
Tags:
  - CI/CD
  - Workflow
  - Supply Chain
  - Self-Hosted
  - Infrastructure Compromise
Reports:
  MITRE ATT&CK:
    - TA0001:T1195.002  # Supply Chain Compromise: Compromise Software Supply Chain
    - TA0002:T1072  # Execution: Software Deployment Tools
    - TA0008:T1021  # Lateral Movement: Remote Services
    - TA0004:T1134  # Privilege Escalation: Access Token Manipulation
Description: >
  Detects when a pull_request_target workflow runs on a self-hosted runner. pull_request_target workflows
  run with elevated privileges and have access to repository secrets even when triggered by external contributors from forks.
  When these workflows run on self-hosted runners attackers can gain direct code execution on the underlying infrastructure 
  with potential access to internal network, databases, and systems. Unlike GitHub-hosted runners which are destroyed after each job, 
  self-hosted runners persist and can be permanently compromised. This pattern is high risk regardless of whether the PR
  is cross-fork or same-repository because self-hosted runners represent infrastructure access.
  GitHub explicitly warns never to use self-hosted runners with public repositories or workflows
  that can be triggered by untrusted contributors. This configuration allows any GitHub user with
  read access to your repository to execute arbitrary code on your infrastructure.
Runbook: |
  1. Stop the self-hosted runner:
     - SSH/access the runner system
     - Stop the runner service or unregister the runner

  2. Isolate the runner from network:
     - Block outbound connections via firewall
     - Disconnect from internal network if possible

  3. Review the workflow:
     - Disable or delete the workflow file that uses pull_request_target + self-hosted
     - Or modify to use "runs-on: ubuntu-latest"

  4. Check runner system for compromise:
     - Review auth logs for unauthorized access
     - Check network connections: netstat -tunap | grep ESTABLISHED
     - Look for persistence mechanisms (cron, systemd, rc.local)
     - Check for suspicious processes or files
     - Review bash history for malicious commands

  5. Review recent workflow runs:
     - Check all workflows that ran on this runner in past 7 days
     - Look for other suspicious activity
     - Identify if compromise occurred in previous runs

  6. Search for indicators of compromise:
     - Outbound connections to suspicious IPs/domains
     - New user accounts or SSH keys on runner system
     - Modified system files or configurations
     - Cryptocurrency miners or backdoors
     - Data staging areas or compressed archives
Reference: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners
Detection:
  - Group:
      - ID: PullRequestTarget
        RuleID: GitHub.Webhook.PullRequestTargetUsage
      - ID: SelfHostedRunner
        RuleID: GitHub.Webhook.SelfHostedRunnerUsed
    MatchCriteria:
      field_name:
        - GroupID: PullRequestTarget
          Match: workflow_run.id
        - GroupID: SelfHostedRunner
          Match: workflow_job.run_id
    EventEvaluationOrder: Chronological
    LookbackWindowMinutes: 1800
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 10
Tests:
  - Name: pull_request_target with self-hosted runner
    ExpectedResult: true
    RuleOutputs:
      - ID: PullRequestTarget
        Matches:
          workflow_run.id:
            "12345678":
              - "2025-10-15T18:31:00Z"
      - ID: SelfHostedRunner
        Matches:
          workflow_job.run_id:
            "12345678":
              - "2025-10-15T18:31:06Z"

  - Name: pull_request_target without self-hosted runner
    ExpectedResult: false
    RuleOutputs:
      - ID: PullRequestTarget
        Matches:
          workflow_run.id:
            "12345678":
              - "2025-10-15T18:31:00Z"

  - Name: Self-hosted runner without pull_request_target
    ExpectedResult: false
    RuleOutputs:
      - ID: SelfHostedRunner
        Matches:
          workflow_job.run_id:
            "12345678":
              - "2025-10-15T18:31:06Z"

  - Name: pull_request_target and self-hosted in different workflows
    ExpectedResult: false
    RuleOutputs:
      - ID: PullRequestTarget
        Matches:
          workflow_run.id:
            "12345678":
              - "2025-10-15T18:31:00Z"
      - ID: SelfHostedRunner
        Matches:
          workflow_job.run_id:
            "87654321":
              - "2025-10-15T18:31:06Z"

  - Name: Multiple self-hosted jobs in same pull_request_target workflow
    ExpectedResult: true
    RuleOutputs:
      - ID: PullRequestTarget
        Matches:
          workflow_run.id:
            "12345678":
              - "2025-10-15T18:31:00Z"
      - ID: SelfHostedRunner
        Matches:
          workflow_job.run_id:
            "12345678":
              - "2025-10-15T18:31:06Z"
              - "2025-10-15T18:32:00Z"

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.