Skip to content
Published on

開発者トークンが漏れる場所 — VSCode 1クリックトークン窃取事件とシークレット衛生

Authors

はじめに — クリック一回でトークンが消えた

2026年6月、Hacker Newsで話題になったセキュリティ分析記事があります。VSCodeのあるバグを悪用し、被害者がリンクをクリックするというたった一回の動作だけでGitHubトークンを窃取できた、という内容です。セキュリティ研究者のAmmar Askarが公開したこの分析は、「自分のIDEが自分を裏切りうる」という不快な真実を改めて思い起こさせました。

核心は、IDEとブラウザ、そしてOAuthフローの間の境界が思ったよりも脆いという点です。私たちはトークンを「うまく隠せばよい」と考えますが、実際にはトークンは自覚していない数多くの経路で漏れ出します。平文の設定ファイル、環境変数、シェル履歴、そして今回の事件のようにIDEのURLハンドラーまで。

本記事は2部構成です。まずVSCodeトークン窃取事件の攻撃チェーンを概念レベルで解剖し、次に開発者が日常的に適用できるシークレット衛生戦略を設定例中心に整理します。結論を先に述べるとこうなります。トークンをうまく隠すことよりも、トークンの権限を減らし、寿命を短くし、いっそなくす方向のほうがはるかに強力です。

VSCode 1クリックトークン窃取の攻撃チェーン

具体的なエクスプロイトコードの代わりに、どのような構造的欠陥が結合して事故が可能になったのかを概念レベルで説明します。防御を理解するには、攻撃の形を知る必要があるからです。

[1] 被害者が悪意あるリンクをクリック (メール、チャット、Webページ)
        |
        v
[2] カスタムURLスキーム (vscode://) でIDEがフォアグラウンドに呼び出される
        |
        v
[3] IDE拡張または内蔵ハンドラーがURLパラメータを十分に検証しない
        |
        v
[4] OAuthコールバック/認証フローが攻撃者の制御するリダイレクトへ誘導される
        |
        v
[5] GitHub認証トークンが攻撃者のエンドポイントへ渡される
        |
        v
[6] 攻撃者がトークンで被害者のリポジトリにアクセス

このチェーンで注目すべき構造的弱点は3つです。

第一に、カスタムURLスキームは強力ですが危険です。vscode://のようなスキームは、ブラウザがOSを介してIDEを直接呼び出せるようにします。このとき渡されるパラメータをIDE側が信頼してしまうと、外部からIDEの内部動作を操作できるようになります。

第二に、OAuthリダイレクト検証の盲点です。OAuthは認証後にトークンを特定のリダイレクトURIに返しますが、このURI検証が緩い場合(部分文字列マッチ、ワイルドカード許可など)、攻撃者が自分のエンドポイントへトークンを横取りできます。

第三に、ユーザー操作の最小化が逆説的に危険です。「ワンクリック」の利便性のために確認ステップを省略すると、被害者は自分が何を承認したのか認識する機会を失います。

この事件の教訓は明確です。どのようなトークンであれ一度露出したら、その時点で侵害されたものとみなすべきであり、したがって私たちは「露出しても被害が小さい」トークンを発行し運用すべきです。

開発環境でトークンはどこに住むのか

まず現実を直視しましょう。平凡な開発者のマシンでトークンが保管される場所を列挙すると次のとおりです。

保管場所セキュリティ水準リスク
平文設定ファイル (.npmrc, .git-credentials)非常に低いディスクアクセス、バックアップ、同期で流出
環境変数 (.bashrc, .zshrc export)低い子プロセスへ伝播、ログに露出
.envファイル低い誤ってコミット、Dockerイメージに混入
OSキーチェーン (macOS Keychainなど)高いアプリの権限モデルに依存
シークレットマネージャー (Vault, クラウドKMS)非常に高い運用の複雑さ

ほとんどの流出事故は上の表の上3行で発生します。その中でも最も多いのが、環境変数にトークンをexportする習慣です。

# アンチパターン — 絶対にこうしないでください
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
export AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxx

この方式の問題は、トークンがすべての子プロセスに自動継承される点です。何気なく実行したスクリプト、npmパッケージのinstall script、さらには侵害されたCLIツールまでもがこのトークンを読めます。先述のnpmサプライチェーン攻撃で環境変数が主要な窃取対象になる理由がまさにこれです。

基本原則はこうです。トークンは必要なプロセスにのみ、必要な時点にのみ注入し、可能であればOSキーチェーンやシークレットマネージャーに委ねます。

PATの権限設計 — classic vs fine-grained

GitHub Personal Access Tokenには2種類あります。この2つの違いを理解することがトークンセキュリティの出発点です。

区分Classic PATFine-grained PAT
権限範囲スコープ単位 (repo, workflowなど)リポ別 + 細分化された権限
対象リポアカウントの全リポ選択したリポのみ
有効期限設定可能 (無期限も許可)最大1年、有効期限必須
組織承認限定的組織ポリシーで制御可能
推奨度非推奨 (レガシー)推奨

Classic PATのrepoスコープは事実上、アカウントの全リポジトリに対する読み取り/書き込み権限を与えます。トークン一つが流出すると全リポが露出する構造です。一方、Fine-grained PATは「このリポに対して、この権限のみ」を明示できます。

権限設計の原則は単純です。

1. 最小権限: 作業に必要な最小限の権限のみ付与
2. 最小範囲: 対象リポを明示的に選択
3. 短い寿命: 可能な限り短い有効期限 (例: 7日、30日)
4. 単一目的: 一つのトークンは一つの用途のみ
5. 追跡可能: トークンに用途がわかる名前を付与

例えばCIで特定のリポにリリースをプッシュするトークンであれば、contents:writeとそのリポ一つを選択すれば十分です。issuesや他のリポに対する権限はまったく必要ありません。

トークンの寿命とローテーション

トークンセキュリティで最も過小評価されるのが寿命です。無期限トークンは時限爆弾です。いつ流出したかも分からないまま数年間有効なトークンが、事故の定番の原因です。

ローテーション戦略を整理すると次のとおりです。

- 個人PAT: 90日以内に有効期限 + 四半期ごとにローテーション
- CI/自動化トークン: 30日以内、またはOIDCで代替 (後述)
- サービスアカウントトークン: シークレットマネージャーで自動ローテーション
- 流出疑い時: 即座に破棄 + 新トークン発行、例外なし

ローテーションを自動化しなければ、結局誰もやりません。GitHub CLIを使えば、トークン状態を定期的に点検するスクリプトを置けます。

# 現在の認証状態とトークンスコープを確認
gh auth status

# トークンをキーチェーンなど安全なストレージに委譲 (平文ファイルを回避)
gh auth login --hostname github.com --git-protocol https

GitHub Secret ScanningとPush Protection

GitHubはリポジトリにプッシュされるコードから、既知の形式のシークレットを自動検知します。核心はpush protectionです。シークレットがコミットに含まれるとプッシュ自体を拒否し、流出を発生前に遮断します。

組織/リポレベルで有効化するほかに、ローカルでも同じ保護を受けるには事前遮断ツールを併用するのがよいです。次はGitHubのpush protectionがどう動作するかを示す概念図です。

git push
   |
   v
[GitHubサーバー] -- プッシュされるdiffからシークレットパターンを検知
   |
   +-- シークレット発見 --> プッシュ拒否、開発者に位置を案内
   |
   +-- クリーン --> プッシュ成功

すでに流出した場合の対応もGitHubが一部サポートします。パートナープログラムに登録されたトークン形式(例: クラウドキー)が公開リポで発見されると、GitHubが該当サービス提供者に通知し自動破棄を誘導します。しかしこれは最後の安全網であって第一の防御ではありません。シークレットはそもそもコミットされてはなりません。

.env流出を防ぐ3重防御

.envファイルは開発者シークレット流出の第一位の経路です。3重で防ぎます。

ステップ1 — gitignore

最も基本ですが、最も頻繁に漏れます。

# .gitignore
.env
.env.*
!.env.example
*.pem
*.key
.git-credentials

下から2番目の関連行の否定パターンが重要です。.env.exampleはコミットしてチームがどの変数が必要かを共有しつつ、実際の値が入ったファイルはすべて遮断します。

ステップ2 — pre-commit hookで事前遮断

コミット時点でシークレットをスキャンして遮断します。gitleaksが事実上の標準です。

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

インストール後、次でフックを登録します。

pip install pre-commit
pre-commit install
# これで毎コミットごとにgitleaksが自動実行される

ステップ3 — gitleaksカスタム設定

デフォルトルールセットのほかに、社内トークン形式を追加できます。

# .gitleaks.toml
title = "社内gitleaks設定"

[extend]
useDefault = true

[[rules]]
id = "internal-api-key"
description = "社内APIキーパターン"
regex = '''myco_(live|test)_[0-9a-zA-Z]{32}'''
tags = ["key", "internal"]

[allowlist]
description = "テストフィクスチャを許可"
paths = [
  '''test/fixtures/.*''',
]

CIでも全履歴をスキャンして二重に点検します。

# .github/workflows/gitleaks.yml
name: Secret Scan
on: [pull_request]

permissions:
  contents: read

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

git-credential helperの安全な設定

gitが資格情報をどう保管するかは、credential.helper設定にかかっています。最も危険なのはstoreヘルパーです。

# 危険 — 資格情報を平文で~/.git-credentialsに保存
git config --global credential.helper store

この設定はトークンをホームディレクトリに平文で記録します。バックアップ、クラウド同期、他プロセスのディスクアクセスでそのまま流出します。代わりにOS別の安全なヘルパーを使います。

# macOS — Keychainに委譲
git config --global credential.helper osxkeychain

# Windows — Credential Manager
git config --global credential.helper manager

# Linux — libsecret (GNOME Keyringなど)
git config --global credential.helper libsecret

# またはキャッシュのみ使用 (メモリに短時間保管、ディスク記録なし)
git config --global credential.helper 'cache --timeout=3600'

さらに、資格情報をホスト別に分離すれば、一つのホストが侵害されても他のホストは安全です。

# ~/.gitconfig
[credential "https://github.com"]
    helper = osxkeychain
[credential "https://gitlab.internal.example.com"]
    helper = osxkeychain

IDE拡張のサプライチェーンリスク

VSCode事件の本質は、拡張エコシステムの信頼モデルの問題でもあります。拡張はIDEの全権限で実行されます。ファイルシステムの読み書き、ネットワーク通信、シェルコマンド実行、そして他の拡張が保管したシークレットへのアクセスまで可能です。

拡張の権限モデルの現実を整理するとこうなります。

インストール済みVSCode拡張の能力:
  - ワークスペースの全ファイルを読み取り (.env含む)
  - 任意のネットワークリクエストを送信
  - 統合ターミナルでコマンドを実行
  - SecretStorage APIで保管されたトークンにアクセス (拡張間の隔離はあるが完全ではない)
  - 自動更新 — 昨日安全だった拡張が今日悪意あるものになりうる

実務ガイドラインは次のとおりです。

[ ] 拡張は発行者(publisher)の身元が検証されたものだけインストール
[ ] ダウンロード数、更新頻度、オープンソースか否かを確認
[ ] ワークスペース信頼(Workspace Trust)機能を有効化 — 信頼しないフォルダで拡張を制限
[ ] 使わない拡張は無効化/削除
[ ] 機密プロジェクトは別プロファイルまたは別マシンで作業
[ ] 拡張の自動更新を切り、変更を検討 (高機密環境)

ワークスペース信頼は特に重要です。クローンした見知らぬリポジトリを開くとき、そのフォルダの設定が自動的にコードを実行できないように防いでくれます。

CIシークレット — OIDCで長期トークンをなくす

ここまでがトークンを安全に扱う方法だったとすれば、今度はトークンをいっそなくす方法を見ます。CIからクラウドにデプロイするとき、従来は長期アクセスキーをCIシークレットに保存していました。このキーが流出すれば終わりです。

OIDCフェデレーション(OpenID Connect federation)はこの問題を根本的に解決します。CI実行ごとにクラウド提供者が短期資格情報を発行し、ジョブが終わると失効します。保存された長期秘密が存在しません。

[従来方式 — 長期キー]
  CIシークレットにAWSアクセスキーを保存 --> 流出時に永久侵害

[OIDC方式 — 短期トークン]
  GitHub ActionsがOIDCトークンを発行
        |
        v
  クラウドがOIDCトークンの発行者/リポ/ブランチを検証
        |
        v
  検証通過時に短期資格情報を発行 (数十分で失効)
        |
        v
  ジョブ終了とともに資格情報が消滅

GitHub ActionsからAWSにOIDCで接続する設定例です。

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

permissions:
  id-token: write   # OIDCトークン発行に必須
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-deploy
          aws-region: ap-northeast-2
          # アクセスキーなし — OIDCで役割を直接引き受け
      - run: aws s3 sync ./dist s3://my-bucket/

クラウド側では信頼ポリシーで、どのリポ/ブランチがこの役割を引き受けられるかを制限します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
        }
      }
    }
  ]
}

ここでsub条件が核心的な防御です。特定のリポの特定のブランチで実行されたワークフローのみがこの役割を引き受けられます。フォークされたPRや他のリポでは資格情報を受け取れません。条件を緩くしすぎると(例: 全ブランチのワイルドカード)意味がないので注意が必要です。

GCP、Azureも同じパターン(Workload Identity Federation, federated credentials)を提供します。新規プロジェクトなら、長期キーを発行する前にOIDCが可能かをまず確認するのが正しいです。

インシデント対応runbook

トークン流出が疑われるときの手順です。普段から暗記する必要はありませんが、どこかに書かれているべきです。

[即座] 破棄
  - 流出/疑いトークンを即座にrevoke (GitHub Settings, クラウドコンソール)
  - 破棄が新トークン発行より先 — 露出ウィンドウを最小化

[5分] 影響範囲の把握
  - 該当トークンの権限範囲を確認 (どのリポ、どの作業が可能だったか)
  - GitHub監査ログ / クラウドCloudTrailで異常活動を照会
  - トークンで可能だった行為を一覧化 (push, パッケージpublish, リソース作成)

[30分] 封じ込めと点検
  - 同じトークンを使っていた全システムでトークンを交換
  - 疑わしいコミット/リリース/デプロイを特定・検討
  - 新しいSSHキー、deploy keyの追加有無を点検 (バックドア)

[事後] 再発防止
  - 平文保管経路を除去、キーチェーン/シークレットマネージャーへ移行
  - 可能なトークンをOIDCで代替
  - gitleaks pre-commit + CIスキャンの導入を確認
  - git履歴にシークレットが残っていれば履歴の書き換えを検討

最後の項目には注意が必要です。シークレットがコミット履歴に一度入ると、ファイルを削除した新コミットを追加しても過去のコミットにはそのまま残ります。履歴の書き換え(git filter-repoなど)が必要で、何より露出したトークンは書き換えの有無に関わらず破棄が優先です。

チェックリスト

個人開発者:

[ ] 環境変数に長期トークンをexportしない
[ ] git credential.helperをstoreではなくOSキーチェーンに設定
[ ] PATはfine-grainedで、最小権限 + 有効期限設定
[ ] .gitignoreに.env、*.pem、*.key、.git-credentialsを含める
[ ] gitleaks pre-commit hookをインストール
[ ] VSCodeワークスペース信頼を有効化、拡張を最小化
[ ] 四半期ごとにトークンを点検・ローテーション

組織:

[ ] secret scanning + push protectionを全リポで有効化
[ ] CIの長期キーをOIDCフェデレーションで代替
[ ] fine-grained PATポリシー + classic PAT制限
[ ] gitleaksをPR必須チェックとして登録
[ ] トークン流出インシデント対応runbookを文書化
[ ] 開発者オンボーディングにシークレット衛生教育を含める

落とし穴と反論

第一に、キーチェーンも万能ではありません。キーチェーンはOSがロック状態のときのみ保護されます。マシンがロック解除された状態で悪意あるコードが実行されると、正常なアプリ権限でキーチェーンを読めます。キーチェーンは平文ファイルよりはるかに優れていますが、マシン自体が侵害されると限界があります。

第二に、OIDCも万能ではありません。sub条件を緩く設定すると(ブランチワイルドカード、環境未指定)、フォークPRや意図しないワークフローが資格情報を受け取れます。OIDCの安全性は信頼ポリシーの厳格さに全面的にかかっています。導入したという事実よりも、条件を正確に絞ったかが重要です。

第三に、gitleaksはパターンベースです。既知の形式のシークレットはよく捕捉しますが、形式が不規則な社内秘密やbase64でエンコードされて迂回された値は見逃すことがあります。カスタムルールを加えても完璧ではないので、検知ツールは最後の防衛線であって免罪符ではありません。

第四に、利便性とセキュリティの緊張は永遠です。トークンの寿命を短くするとローテーション負担が増え、権限を狭めると作業が止まります。この緊張を無視したポリシーは迂回を生みます。自動化(自動ローテーション、OIDC、シークレットマネージャー連携)でセキュリティのコストを下げることが、持続可能な唯一の道です。

第五に、今回のVSCode事件が示すように、私たちが信頼するツール自体が攻撃対象領域です。IDE、拡張、CLI、パッケージマネージャーすべてがトークンにアクセスします。トークンを扱うツールの数を減らし、各ツールの権限を減らすことが根本的な方向です。

おわりに

VSCode 1クリックトークン窃取事件は、「トークンをどこに隠すか」という問いが間違っていることを示します。正しい問いは「このトークンが漏れても被害が小さくなるようにどう設計するか」です。

3つに要約します。

  1. 権限を減らせ: fine-grained PAT、最小スコープ、単一目的。
  2. 寿命を減らせ: 短い有効期限、定期ローテーション、流出時に即座に破棄。
  3. トークンをなくせ: CIはOIDCで、保管はキーチェーン/シークレットマネージャーで。

この3つの方向がすべて向かう先は一つです。トークンが露出することを完璧に防ぐことはできないので、露出したときの被害を構造的に小さくすることです。それが1クリック時代のシークレット衛生です。

参考資料