CI/CDと信頼の危うさ — GitHub Actionsのハッキング事件から見る教訓

私はいつものように朝のルーティンの一部として、LinkedInを眺めていた。 私はLinkedInにブログも投稿し、情報発信や収集のツールとして活用している。 そのうえ、興味深い情報が時折流れてくるのがいい。 私にとってはFacebookよりも有用である。 個人の盛りと映えがあふれるInstagramは使い物にならない(あくまでも個人の感想だが)。
そんな中、ある投稿が目に留まった。 「CI/CDハッキング」という言葉だ。 30年以上プログラミングをしてきた私でも、初めて聞く組み合わせだった。 そう、CIとハッキングが同じ文脈で語られることに、一瞬違和感を覚えた。
詳細を読んでいくと、2025年3月14日にGitHubの人気アクション「tj-actions/changed-files」が攻撃され、多くのシークレット情報が漏洩したという事件だった。
事件の概要
攻撃者は巧妙な手口を使っていた。 まずBase64でエンコードされたシェルスクリプトを実行し、次のフェーズのペイロードをダウンロード。 そしてPythonとシェルスクリプトを組み合わせてメモリをダンプし、特定の正規表現パターンでシークレット情報を抽出。 Base64で二重エンコードしてログに出力する、という流れだ。
調査結果によると、1104件のワークフロー実行のうち276件で攻撃の痕跡が見つかり、603件のシークレットが流出した。 その77%がGitHubの一時的なトークン(ghs_)だったという。
ため息が出る。 これが現代のサプライチェーン攻撃か。
技術的な詳細:攻撃の手口を解剖する
この攻撃の技術的な側面を分析してみよう。 まず「tj-actions/changed-files」とは何か。 これはGitHub Actionsのワークフローで、コミットやプルリクエストで変更されたファイルを検出するためのアクションだ。 多くの開発者がCI/CDパイプラインで使用している。
sequenceDiagram
participant Attacker as 攻撃者
participant GitHub as GitHubリポジトリ
participant Action as tj-actions/changed-files
participant Runner as GitHub Actions実行環境
participant Memory as メモリ空間
participant Logs as GitHubログ
Attacker->>Action: アクションコードを改ざん
Note over Attacker,Action: 作者アカウント乗っ取りまたはプッシュ権限奪取
GitHub->>Runner: 改ざんされたアクションを実行
Runner->>Runner: Base64エンコードされたペイロードをデコード
Runner->>Attacker: 外部サーバーに接続
Attacker->>Runner: 追加のスクリプトをダウンロード
Runner->>Memory: メモリダンプ処理を実行
Note over Runner,Memory: Pythonとシェルスクリプトでメモリをダンプ
Runner->>Memory: 正規表現で検索
Note over Runner,Memory: [^"]+:"{"value":"[^"]*","isSecret":true}
Memory->>Runner: シークレット情報を抽出
Runner->>Runner: Base64で二重エンコード
Note over Runner: GitHubの自動マスキング機能をバイパス
Runner->>Logs: エンコードされたシークレットをログに出力
Attacker->>Logs: ログからシークレット情報を取得
Attacker->>Attacker: デコードして元のシークレットを復元
攻撃の第一段階は、このアクションのコードを改ざんすること。 おそらく作者のGitHubアカウントが乗っ取られたか、何らかの方法でプッシュ権限を奪取したのだろう。
改ざんされたコードはBase64エンコードされたペイロードを実行し、外部から追加のスクリプトをダウンロードして実行する指示になっていた。
第二段階のメモリダンプ処理は、プロセスのメモリ空間から特定のパターンを検索する処理だ。
GitHub Actionsの実行環境内のメモリからシークレット情報を抽出したとのことだ。
おそらく、正規表現[^"]+":\{"value":"[^"]*","isSecret":true\}
を使用して、
GitHubが内部的に管理しているシークレット情報のパターンを特定しているのだろう。
このパターンはGitHub Actionsがシークレットを保存する際のJSON構造に対応している。 攻撃者はこのパターンに一致する文字列を見つけると、Base64で二重エンコードして出力ログに書き込んだ。 なぜ二重エンコードかというと、GitHub Actionsのログシステムではシークレット値を自動的にマスクする機能があるが、 Base64エンコードすることでこの保護をバイパスしたのだろう。
皮肉な安全策
ここで思い出したのは、自分のGitHub Actionsの運用方法だ。 私は全てのアクションを自前で書いている。 他人のスクリプトを使うという発想がなかった。
「非効率だろう」と言われれば、そうかもしれない。 だが、今回の事件を見ると、むしろ正解だったようだ。 皮肉なものだ。 効率を追求するあまり、セキュリティをないがしろにする世の中の風潮。
メモリダンプと聞いて、懐かしさを感じた。 私は過去にC言語の開発を20年以上行っていた経験がある。 メモリダンプのようなプログラムは、C言語を使えば比較的簡単に実装できる。 低レベルのメモリアクセスが得意な言語だからだ。
安全なCI/CDの実装:LLMとコードの民主化
以前は「汎用的な処理は既存のアクションを使う」というのが効率化の常識だった。 しかし今や状況は変わった。 LLM(大規模言語モデル)の登場により、コードの「民主化」が進んでいる。
私のような長年のエンジニアでなくとも、ChatGPTなどのLLMを使えば、セキュリティを考慮した自前のアクションを簡単に作成できる時代だ。 例えば「変更ファイルを検出するGitHub Actionsの安全なYAMLを作成して」と指示するだけで、外部依存なしの堅牢なスクリプトが得られる。
これにより、サードパーティに依存するリスクを減らしつつ、開発効率も維持できる。 我々はもはや「便利だから」という理由だけで、検証されていない外部コードを使う必要はないのだ。
現場の現実
今の業務委託先の話をすると、もっと笑えない状況がある。 CI/CDパイプラインなんてない。 全てのシステムデプロイは手作業だ。
デプロイ担当者はGitの知識が乏しく、開発担当はバージョン管理がいい加減。 「動けばいい」という精神論が支配している。 Docker関連ファイルや環境変数など、本番環境に必要なファイルがリポジトリにないことも判明した。
私はため息をつきながら、その後始末をしている。 今回のハッキング事件を聞いて、「CI/CDがないのは逆に安全なのかもしれない」と冗談交じりに思ったりもする。 皮肉な話だ。
技術的対策:実装ベストプラクティス
記事には今回の事件を防ぐための具体的な技術対策も紹介されていた。
-
SHAピンニング: バージョン番号ではなく、特定のコミットSHAを指定する。 これにより、後から悪意のあるコードがpushされても影響を受けない。
-
ハニートークン実装: 偽のトークンを仕掛けて、それが使用されたら警告が飛ぶようにする。 これは侵入検知の一種で、不正アクセスの早期発見に役立つ。
-
最小権限の原則: ジョブごとに必要最小限のシークレットのみを渡す。 テスト用ジョブに本番デプロイキーを渡さないなど、権限の分離を徹底する。
-
メモリ保護: 環境変数とシークレットの扱いに注意する。 ログ出力時にはマスキングを行い、不要な情報漏洩を防ぐ。
それに私はこう付け加えたい。
- 自前のアクション: 重要な処理は自分で実装する。 LLMの助けを借りれば、セキュアなコードを短時間で作成できる。
教訓
この事件から学べることは明確だ。
- サードパーティのコードを盲目的に信頼しない
- 可能な限り自前でスクリプトを書く(今はLLMの助けもある)
- コミットSHAをピン留めする(バージョン番号ではなく)
- ログの削除とキーの定期的な変更
- メモリ安全性を考慮したコーディング
技術の進化は便利さをもたらしたが、同時に新たな脆弱性も生み出した。 効率とセキュリティ。 言い換えれば、刺激と安全。
──夜の歌舞伎町で、酔った私に言い寄るキャッチのようなものだ。
言葉は甘く、未知の快楽を囁く。
だが、先に待つのは罠か、あるいは破滅かもしれない。
どちらを取るか迷ったら、後者を選べ。
それが、30年以上ITの世界で生き残ってきた私からの忠告だ。
CI/CDパイプラインの話は面白かったが、現実の問題が山積みだ。 椅子に深く腰掛け、キーボードに手を伸ばす。 さて、今日も業務委託先の後始末に精を出すか。 三つ並んだ27インチモニターに、レガシーコードとDockerファイル、そして修正案が映し出される。 一日の始まりだ。