No results found.

GitHub Webhook Secret Exposure Incident (GH-9951654-7992-a1)

Overview

Between September 2025 and January 2026, GitHub introduced a webhook delivery bug that exposed webhook secrets in an HTTP header (X-Github-Encoded-Secret) for a subset of deliveries.

If your infrastructure (or 3rd party integration) logs HTTP request headers, webhook secrets may be present in those logs.

The issue was resolved on January 26, 2026. GitHub reports no evidence of external compromise.

Required actions

1. Rotate webhook secrets

All affected webhook secrets should be rotated, regardless of suspected exposure.

Documentation: https://docs.github.com/en/webhooks/using-webhooks/editing-webhooks


2. Review and purge logs

Check webhook receivers, API gateways, and observability systems for logged request headers.

If found, purge or restrict access to logs containing:

  • X-Github-Encoded-Secret
  • X-Hub-Signature-256

3. Verify signature validation

Ensure webhook verification uses X-Hub-Signature-256 with the updated secret:

https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries

Some helpful GH CLI commands

A few GH CLI commands to aid with scope analysis, rotation or removal of webhooks in bulk.

Bulk webhook audit (GitHub CLI)

To get an overview of how many webhooks are setup on your repositories, and when/if they have been executed, you can use the following Github CLI command to produce a CSV report.

(
  echo "repo,hook_id,webhook_url,last_delivery"
  gh repo list --limit 1000 --json nameWithOwner -q '.[].nameWithOwner' | while read repo; do
    gh api repos/$repo/hooks --jq '.[] | [.id, .config.url] | @tsv' | while IFS=$'\t' read hook_id hook_url; do

      last_time=$(gh api repos/$repo/hooks/$hook_id/deliveries \
        --jq 'if length == 0 then "never" else .[0].delivered_at end')

      echo "$repo,$hook_id,$hook_url,$last_time"
    done
  done
) | tee webhook_audit.csv

Bulk rotate webhook secrets

⚠️
This will immediately break existing webhook authentication until updated in the receiving system

The following Github CLI command will rotate ALL webhook secrets, across all your repositories. If you want to rotate an explicit list of repos, you can replace the outer call with a hard coded list, or the generated audit csv from the previously.

gh repo list --limit 1000 --json nameWithOwner -q '.[].nameWithOwner' | while read repo; do
  gh api repos/$repo/hooks --jq '.[]' | while read hook; do

    hook_id=$(echo "$hook" | jq -r '.id')
    NEW_SECRET=$(openssl rand -hex 32)

    config=$(echo "$hook" | jq --arg secret "$NEW_SECRET" '.config + {secret: $secret}')
    echo "Rotating secret for $repo hook $hook_id"
    echo "$repo,$hook_id,$NEW_SECRET" >> rotated_webhooks.csv

    gh api --silent \
      -X PATCH repos/$repo/hooks/$hook_id \
      --input - <<< "$(jq -n --argjson config "$config" '{config: $config}')"

  done
done

Bulk delete webhooks

This will permanently delete webhooks and stop all deliveries.

If you have a bunch of redundant Webhooks that are not important, like for example, I had a bunch of Snyk ones that are not being used. You can delete them all in bulk with the following.

gh repo list --limit 1000 --json nameWithOwner -q '.[].nameWithOwner' | while read repo; do
  gh api repos/$repo/hooks --jq '.[]' | while read hook; do
    hook_id=$(echo "$hook" | jq -r '.id')
    echo "Deleting $repo hook $hook_id"
    gh api --silent -X DELETE repos/$repo/hooks/$hook_id
  done
done