2026年4月15日、GitHub は webhook secret が HTTP ヘッダーに含まれた状態で配信されていたバグについて、影響を受けたユーザーへの通知を開始しました。CircleCI の GitHub OAuth 連携を使用しているプロジェクトもこのバグの影響を受ける可能性があります。
本インシデントの詳細は、GitHub から影響を受けた webhook の所有者宛に送信されている通知メールに記載されています。メールが届いていない方や、情報収集が目的の場合は、CircleCI が公式フォーラムに投稿したアナウンスを参照してください。
本記事では、影響の有無を確認する方法、webhook secret をローテーションする手順、および影響を受けるプロジェクトを API で一括リストアップするスクリプトを紹介します。
この記事の対象は GitHub OAuth 連携を使用している CircleCI プロジェクトです。GitHub App 連携のみを使用しているプロジェクトは影響を受けません。連携方式の違いについては「CircleCI と GitHub の連携を OAuth から GitHub App 連携に移行する理由とその方法」を参照してください。
何が起きたか
GitHub の webhook 配信プラットフォームの新バージョンにバグがあり、webhook secret が X-Github-Encoded-Secret という HTTP ヘッダーに base64 エンコードされた状態で、webhook ペイロードと一緒に受信側エンドポイントへ送信されていました。
webhook secret は、受信したペイロードが GitHub から送信されたものであることを検証するための共有鍵です。この値は GitHub と webhook の所有者だけが知っているべきものであり、HTTP ヘッダーに含まれるべきではありません。
影響期間と対応状況は以下のとおりです。
| 項目 | 内容 |
|---|---|
| バグの発生期間 | 2025年9月11日 〜 2025年12月10日、および2026年1月5日に一時的に再発 |
| 修正日 | 2026年1月26日 |
| 影響範囲 | フィーチャーフラグで新プラットフォームに振り分けられた一部の webhook 配信 |
| GitHub の見解 | TLS で暗号化されているため受信エンドポイントでのみアクセス可能。悪用の証拠はなし |
GitHub は TLS による暗号化を理由に「secret が傍受された証拠はない」としていますが、受信側サーバーが HTTP リクエストヘッダーをログに記録していた場合、ログファイルに webhook secret が平文で保存されている可能性があります。そのため、影響を受けた webhook secret は侵害されたものとして扱い、ローテーションすることが推奨されています。
漏洩した場合のリスク
webhook secret が漏洩した場合、攻撃者は正規の HMAC 署名(X-Hub-Signature-256 ヘッダー)を生成できるようになります。これにより、偽の webhook ペイロードを GitHub から送信されたものとして受信側システムに受け入れさせることが可能になります。
CircleCI の場合、webhook secret は GitHub からの push イベント等を受信してパイプラインをトリガーするために使用されています。secret が侵害された場合、不正なペイロードによってパイプラインが意図せずトリガーされる可能性があります。
影響を受けているか確認する
影響を受けているかどうかは、以下の2つの方法で確認できます。
GitHub からの通知メール
GitHub は影響を受けた webhook の所有者・管理者に対して、対象の webhook 一覧を含む通知メールを送信しています。メールの件名は「Action needed: Rotate webhook secrets in your GitHub account」です。メール末尾の「Affected webhooks」セクションに、影響を受けた webhook のリポジトリ名と hook ID が記載されています。
Affected webhooks:
Repository webhooks (1):
hidetaka-cci/practice-vite-app (hook ID: 584469618)
この一覧が、ローテーション対象の特定に必要な情報です。リポジトリ名からどの CircleCI プロジェクトに対応するかを判断し、後述の手順でローテーションを実施してください。
CircleCI ダッシュボードの警告バナー
CircleCI のダッシュボードにログインすると、影響を受ける組織には以下のような赤い警告バナーが表示されます。
バナー内の「Learn how to rotate your secret」リンクから、ローテーション手順のサポート記事に遷移できます。
webhook secret をローテーションする(Web UI)
ローテーションは、CircleCI の Web UI で OAuth トリガーを削除して再作成することで実行できます。トリガーを削除すると GitHub 側の webhook 登録も削除され、トリガーを再作成すると新しい webhook secret で再登録されます。
手順の概要は以下の5ステップです。
- CircleCI Web アプリで Project Settings > Project setup を開きます
- GitHub OAuth パイプラインのトリガー設定を展開します
- 削除する前にトリガーの設定内容(イベント名、イベントソース、フィルター)を記録します
- OAuth トリガーを削除します
- 同じ設定でトリガーを再作成します
OAuth パイプラインのトリガー行をクリックして展開すると、Trigger Details(Event name、Event source)と、削除ボタン(ゴミ箱アイコン)・編集ボタンが表示されます。削除する前に、この画面のイベント設定を記録してください。
削除ボタンをクリックすると、確認のため「DELETE」の入力を求めるダイアログが表示されます。入力後 Delete trigger をクリックします。
削除が完了すると、トリガー欄に「Add a trigger to automate when your pipeline runs.」というメッセージが表示されます。下部の「click here」リンクからデフォルトの OAuth トリガーを再追加できます。
「click here」をクリックするか、カスタム設定を行っていた場合は手動でトリガーを再作成します。Create trigger フォームで、記録しておいたイベント設定と同じ内容を選択して Save をクリックしてください。
トリガーを再作成したら、小さなコミットを push して、パイプラインが正常にトリガーされることを確認してください。GitHub のリポジトリ設定(Settings > Webhooks)で、新しい hook ID が割り当てられていることも確認できます。
各ステップの詳細な操作手順は、CircleCI サポート記事「Rotating the GitHub webhook secret for CircleCI GitHub OAuth project triggers」を参照してください。
影響を受けるプロジェクトを API で網羅的にリストアップする
GitHub からの通知メールに記載された webhook 一覧で対象プロジェクトを特定できますが、組織内に複数の管理者がいる場合や、通知メールを見逃した可能性がある場合は、API を使って網羅的に確認する方法もあります。以下のシェルスクリプトは、フォロー中の CircleCI プロジェクトの中から GitHub OAuth トリガーが設定されているプロジェクトを一括でリストアップします。
#!/usr/bin/env sh
# list-oauth-trigger-projects.sh
#
# CircleCI 上で GitHub OAuth トリガーが設定されているプロジェクトを一覧表示する。
# このスクリプトは「影響範囲の洗い出し」のみを行い、ローテーションは実行しない。
#
# 必須コマンド: curl, jq
# 使い方:
# export CIRCLE_TOKEN=... # パーソナル API トークン
# ./list-oauth-trigger-projects.sh
#
# # 特定の org に絞り込む場合:
# ./list-oauth-trigger-projects.sh my-org-name
#
# 出力形式(TSV):
# PROJECT_SLUG PIPELINE_DEF_ID TRIGGER_ID EVENT_PRESET
set -eu
API_V1="${CIRCLECI_API_V1_ROOT:-https://circleci.com/api/v1.1}"
API_V2="${CIRCLECI_API_V2_ROOT:-https://circleci.com/api/v2}"
ORG_FILTER="${1:-}"
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
info() { printf '%s\n' "$*" >&2; }
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; }
need_cmd curl
need_cmd jq
[ -n "${CIRCLE_TOKEN:-}" ] || die "set CIRCLE_TOKEN"
b64dec() {
if base64 --help 2>&1 | grep -q '\-d'; then
base64 -d
else
base64 --decode
fi
}
curl_cci() {
curl -sS -f \
-H "Circle-Token: ${CIRCLE_TOKEN}" \
-H "Accept: application/json" \
"$@"
}
uri_escape() { jq -nr --arg s "$1" '$s | @uri'; }
# Step 1: フォロー中のプロジェクト一覧を取得(v1.1 API)
info "=== Fetching followed projects... ==="
PROJECTS_JSON="$(curl_cci "${API_V1}/projects")" \
|| die "GET /projects failed (check your token)"
PROJECT_SLUGS="$(printf '%s' "$PROJECTS_JSON" | jq -r '
.[]
| select(.vcs_type == "github")
| { slug: ("gh/" + .username + "/" + .reponame), org: .username }
| @base64
')"
TOTAL="$(printf '%s' "$PROJECT_SLUGS" | grep -c . || true)"
info "GitHub projects (followed): ${TOTAL}"
[ -n "$ORG_FILTER" ] && info "org filter: ${ORG_FILTER}"
# Step 2: 各プロジェクトの pipeline definitions / triggers を確認
info ""
COUNT=0
RESULT_FILE="$(mktemp)"
trap 'rm -f "$RESULT_FILE"' EXIT
for encoded in $PROJECT_SLUGS; do
row="$(printf '%s' "$encoded" | b64dec 2>/dev/null)" || continue
slug="$(printf '%s' "$row" | jq -r '.slug')"
org="$(printf '%s' "$row" | jq -r '.org')"
[ -n "$ORG_FILTER" ] && [ "$org" != "$ORG_FILTER" ] && continue
COUNT=$((COUNT + 1))
info " [${COUNT}/${TOTAL}] ${slug} ..."
ENC_SLUG="$(uri_escape "$slug")"
PROJ_JSON="$(curl_cci "${API_V2}/project/${ENC_SLUG}" 2>/dev/null)" || continue
PROJECT_ID="$(printf '%s' "$PROJ_JSON" | jq -r '.id // empty')"
[ -n "$PROJECT_ID" ] || continue
DEFS_JSON="$(curl_cci \
"${API_V2}/projects/${PROJECT_ID}/pipeline-definitions" 2>/dev/null)" || continue
OAUTH_DEFS="$(printf '%s' "$DEFS_JSON" | jq -r '
.items[] | select(.config_source.provider == "github_oauth") | .id
')"
[ -n "$OAUTH_DEFS" ] || continue
for def_id in $OAUTH_DEFS; do
TRIG_JSON="$(curl_cci \
"${API_V2}/projects/${PROJECT_ID}/pipeline-definitions/${def_id}/triggers" \
2>/dev/null)" || continue
printf '%s' "$TRIG_JSON" | jq -r --arg slug "$slug" --arg def "$def_id" '
.items[]
| select(.event_source.provider == "github_oauth")
| [$slug, $def, .id, (.event_preset // "custom")]
| @tsv
' | tee -a "$RESULT_FILE"
done
sleep 0.3
done
FOUND="$(grep -c . "$RESULT_FILE" 2>/dev/null || true)"
info ""
info "=== Done ==="
info "Checked: ${COUNT} projects"
info "OAuth triggers found: ${FOUND}"
if [ "$FOUND" -gt 0 ]; then
info ""
info "=== OAuth triggers (table) ==="
{
printf '%s\t%s\t%s\t%s\n' "PROJECT_SLUG" "PIPELINE_DEF_ID" "TRIGGER_ID" "EVENT_PRESET"
cat "$RESULT_FILE"
} | column -t -s "$(printf '\t')" >&2
fi
このスクリプトは以下の処理を行います。
- CircleCI v1.1 API(
GET /projects)でフォロー中の GitHub プロジェクトの一覧を取得します - 各プロジェクトの pipeline definitions を v2 API で取得し、
config_source.providerがgithub_oauthであるものを抽出します - 該当する定義のトリガーを確認し、GitHub OAuth トリガーが設定されているプロジェクトを整形されたテーブルとして出力します
実行結果は以下のようになります。
=== Done ===
Checked: 35 projects
OAuth triggers found: 12
=== OAuth triggers (table) ===
PROJECT_SLUG PIPELINE_DEF_ID TRIGGER_ID EVENT_PRESET
gh/my-org/web-app a1b2c3d4-e5f6-7890-abcd-ef1234567890 f1e2d3c4-b5a6-7890-abcd-ef1234567890 all-pushes
gh/my-org/api-server b2c3d4e5-f6a7-8901-bcde-f12345678901 e2d3c4b5-a6f7-8901-bcde-f12345678901 all-pushes
gh/my-org/mobile-app c3d4e5f6-a7b8-9012-cdef-123456789012 d3c4b5a6-f7e8-9012-cdef-123456789012 all-pushes
gh/my-org/infrastructure d4e5f6a7-b8c9-0123-defa-234567890123 c4b5a6f7-e8d9-0123-defa-234567890123 all-pushes
gh/my-org/docs e5f6a7b8-c9d0-1234-efab-345678901234 b5a6f7e8-d9c0-1234-efab-345678901234 all-pushes
PROJECT_SLUG 列がローテーション対象のプロジェクトです。この一覧をもとに、Web UI またはサポート記事のリファレンススクリプトでローテーションを実施してください。
このスクリプトはフォロー中のプロジェクトのみを対象とします。フォローしていないプロジェクトは検出できないため、組織内の全プロジェクトを網羅する必要がある場合は、事前に対象プロジェクトをフォローしてください。
webhook secret を API でローテーションする
プロジェクト数が多い場合は、CircleCI API を使用してローテーションを自動化できます。CircleCI サポート記事では、トリガーの削除と再作成を API 経由で実行するリファレンスシェルスクリプト(rotate-oauth-trigger.sh)が提供されています。
使い方
スクリプトは環境変数で対象プロジェクトを指定します。CIRCLE_TOKEN と CIRCLECI_PROJECT_SLUG の2つが必須です。
export CIRCLE_TOKEN=CCIPAT_xxxxxxxxxxxx
export CIRCLECI_PROJECT_SLUG=gh/my-org/web-app
まず DRY_RUN=1 を指定して、削除・再作成の対象が正しいことを確認してください。DRY_RUN を設定すると、実際の削除・再作成は実行されず、実行予定の操作が表示されます。
DRY_RUN=1 bash ./rotate-oauth-trigger.sh
Project id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Pipeline definition id: f1e2d3c4-b5a6-7890-abcd-ef1234567890
Trigger id: d3c4b5a6-f7e8-9012-cdef-123456789012
Recreate body:
{
"event_source": {
"provider": "github_oauth",
"repo": {
"external_id": "123456789"
}
},
"event_preset": "all-pushes"
}
DRY_RUN set — no DELETE/POST performed. Would run:
DELETE https://circleci.com/api/v2/projects/a1b2c3d4-.../triggers/d3c4b5a6-...
POST https://circleci.com/api/v2/projects/a1b2c3d4-.../pipeline-definitions/f1e2d3c4-.../triggers
問題がなければ、DRY_RUN を外して実行します。
bash ./rotate-oauth-trigger.sh
実行が成功すると、新しいトリガーの JSON と新しいトリガー ID が出力されます。
{
"id": "new-trigger-id-xxxx-xxxx-xxxxxxxxxxxx",
"event_source": {
"provider": "github_oauth",
...
},
...
}
New trigger id: new-trigger-id-xxxx-xxxx-xxxxxxxxxxxx
処理の流れ
このスクリプトの処理は Web UI での手動手順と同等で、以下の流れで動作します。
-
GET /projects/{id}/pipeline-definitions/{def_id}/triggersで既存のトリガー設定を取得します -
DELETE /projects/{id}/triggers/{trigger_id}でトリガーを削除します(GitHub 側の webhook も削除されます) -
POST /projects/{id}/pipeline-definitions/{def_id}/triggersで同じ設定のトリガーを再作成します(新しい webhook secret で登録されます)
複数のプロジェクトに対して連続実行する場合は、前節のリストアップスクリプトの出力から PROJECT_SLUG を取り出してループで処理できます。
# リストアップ結果を使って全プロジェクトをローテーション(例)
# まず結果をファイルに保存: ./list-oauth-trigger-projects.sh > result.tsv
cat result.tsv | while IFS="$(printf '\t')" read -r slug def_id trig_id preset; do
CIRCLECI_PROJECT_SLUG="$slug" \
PIPELINE_DEFINITION_ID="$def_id" \
TRIGGER_ID="$trig_id" \
bash ./rotate-oauth-trigger.sh
done
この一括実行は必ず DRY_RUN=1 で事前確認してから実行してください。
リファレンススクリプトの取得については、サポート記事を参照してください。
GitHub App 連携への移行について
今回のインシデントでは、GitHub App 連携を使用しているプロジェクトは影響を受けていません。CircleCI は公式に「GitHub App integrations are not affected」と案内しています。
両連携方式の webhook 管理構造には以下の違いがあります。
| GitHub OAuth 連携 | GitHub App 連携 | |
|---|---|---|
| webhook の管理単位 | リポジトリごとに個別の webhook と secret | GitHub App レベルで1つの webhook(CircleCI が管理) |
| webhook の可視性 | リポジトリの Settings > Webhooks で確認・編集可能 | リポジトリ設定画面には表示されない |
| secret ローテーションの実行者 | 各プロジェクトの管理者が個別に実施 | CircleCI(アプリオーナー)が管理 |
今回の webhook secret 漏洩は、GitHub の新しい webhook 配信プラットフォームにフィーチャーフラグで振り分けられた一部の配信に限定されており、CircleCI の GitHub App webhook はこのフラグの対象外でした。GitHub App 連携が構造的にこの種のバグの影響を受けないことを保証するものではありません。
ただし、OAuth 連携がリポジトリ単位の webhook secret に依存する構造上、今回のようなインシデント発生時にローテーション作業が各プロジェクト管理者に分散する点は、運用上の課題として認識しておく必要があります。
GitHub App 連携には、webhook の管理構造とは別に、以下のセキュリティ上のメリットがあります。
- 短期トークン: リソースアクセスに有効期限の短いトークンを使用します
- ファイングレインド権限: リポジトリ単位でアクセス権を制御できます
- 選択的アクセス: CircleCI がアクセスできるリポジトリを組織単位で限定できます
GitHub App 連携は既存の OAuth 連携と並行して利用でき、リポジトリ単位で段階的に移行できます。詳細な移行手順については以下の記事を参照してください。
まとめ
GitHub の webhook 配信プラットフォームのバグにより、2025年9月から2026年1月の間に一部の webhook secret が HTTP ヘッダーに含まれた状態で配信されていました。CircleCI で GitHub OAuth 連携を使用しているプロジェクトは、webhook secret のローテーションが必要です。
対応手順は以下のとおりです。
- GitHub からの通知メールで影響を受けた webhook のリポジトリと hook ID を確認します
- CircleCI ダッシュボードにバナーが表示されている場合も対応が必要です
- メールの一覧をもとに、Web UI またはサポート記事のリファレンススクリプトで webhook secret をローテーションします
- 組織内の全プロジェクトを網羅的に確認する場合は、本記事のリストアップスクリプトを使用します
- ローテーション後、パイプラインが正常にトリガーされることを確認します
詳細情報は以下の公式リソースを参照してください。
- Rotating the GitHub webhook secret for CircleCI GitHub OAuth project triggers(CircleCI サポート記事)
- Editing webhooks(GitHub Docs)
- Validating webhook deliveries(GitHub Docs)





