問題
フロントエンドのビルド成果物(JavaScript バンドル、CSS、HTML など)に API キーやトークンが埋め込まれたままデプロイされるケースが増えています。ソースコード上で環境変数やビルド時置換を利用していても、ビルド後のファイルに 実際のシークレット文字列 が残っていると、公開されたエンドポイントから簡単に取得されてしまいます。
- ソースコードレビューだけでは検出できない
- CI/CD のビルドステップで環境変数が正しく置換されていないと漏洩リスクが顕在化
- クライアント側に公開すべきでない認証情報が混入すると、攻撃者に即座に利用される可能性がある
このような漏洩は、ビルド成果物を直接スキャン することで初めて検出できます。
メカニズム
-
ビルド成果物の取得
- CI パイプラインの最後のステップで、
dist/やpublic/ディレクトリに出力されたファイルを対象にする。
- CI パイプラインの最後のステップで、
-
シークレットパターンの定義
- API キーやトークンは一定の文字列長やプレフィックス(例:
pk_live_,sk_test_)を持つことが多いので、正規表現でパターン化する。 - 公開鍵や公開設定のキーは「正当な公開キー」として除外リストに登録し、誤検知を防ぐ。
- API キーやトークンは一定の文字列長やプレフィックス(例:
-
スキャンエンジンの実行
- テキスト検索エンジン(例:
ripgrep,git grep, カスタム正規表現マッチャー)で全ファイルを走査。 - バイナリファイルは Base64 デコード後に再検索することで、圧縮やミニファイされたコード中のシークレットも捕捉できる。
- テキスト検索エンジン(例:
-
結果のフィルタリング
- 除外リストにマッチしたものは「公開キー」としてマークし、残りを「潜在的シークレット」としてレポート。
- ファイルパス、行番号、マッチ文字列を出力し、CI のアーティファクトとして保存するか、プルリクエストにコメントする。
実装例
以下は Node.js 環境で、dist/ ディレクトリ内の全テキストファイルをスキャンし、疑わしいシークレットをレポートする最小構成です。
# install dependencies
npm i -D fast-glob
// scan-build.js
// ビルド成果物からシークレットを検出するスクリプト
const fg = require('fast-glob');
const fs = require('fs');
const path = require('path');
// 1. スキャン対象ファイル(テキスト系)を取得
const files = fg.sync(['dist/**/*.js', 'dist/**/*.css', 'dist/**/*.html'], {
ignore: ['**/node_modules/**'],
});
// 2. シークレットパターン(例: Stripe の公開キー、Firebase の API キー)
const secretPatterns = [
// Stripe 公開キーは `pk_live_` または `pk_test_` で始まる
/pk_(live|test)_[a-zA-Z0-9]{24,}/g,
// Firebase API キーは 39 文字の英数字
/AIza[0-9A-Za-z-_]{35}/g,
];
// 3. 除外リスト(正当な公開キー)
const whitelist = new Set([
// 例: 本番環境で意図的に公開しているキー
'pk_live_51H8...example',
]);
// 4. スキャン実行
let findings = [];
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
secretPatterns.forEach((regex) => {
let match;
while ((match = regex.exec(content)) !== null) {
const secret = match[0];
if (!whitelist.has(secret)) {
findings.push({
file,
line: content.slice(0, match.index).split('\n').length,
secret,
});
}
}
});
}
// 5. 結果出力
if (findings.length === 0) {
console.log('🔍 シークレットは検出されませんでした。');
} else {
console.log('⚠️ 潜在的シークレットが検出されました:');
findings.forEach(({ file, line, secret }) => {
console.log(`- ${file}:${line} → ${secret}`);
});
// CI 失敗させる例
process.exit(1);
}
CI への組み込み例(GitHub Actions)
name: Scan Build Artifacts
on:
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install deps
run: npm ci
- name: Build
run: npm run build # dist/ が生成されることを想定
- name: Scan for secrets
run: node scan-build.js
この構成では、ビルドが成功した後に自動的にシークレットスキャンが走り、検出された場合はジョブが失敗します。開発者はプルリクエストのチェック結果で即座に問題を把握できます。
注意点
| 項目 | 説明 |
|---|---|
| 除外リストの管理 | 正当な公開キーは必ずホワイトリストに追加し、誤検知でビルドが止まらないようにする。 |
| パターンの過剰検出 | 正規表現が広すぎると文字列の一部がシークレットと誤判定される。実際に使用しているキー形式に合わせて調整すること。 |
| バイナリ/圧縮ファイル | 圧縮やミニファイされたコードは文字列が分断されることがある。必要に応じてデコードやデミニファイ処理を追加する。 |
| スキャンコスト | 大規模リポジトリや多数のビルド成果物を対象にするとスキャン時間が増大する。対象ファイルを絞るか、インクリメンタルスキャンを検討する。 |
| シークレットの有効期限 | たとえ検出しても、すでにローテーション済みのキーが残っているケースがある。検出後 |
Details, and a free check: https://keydrift.dev/blog/next-public-and-framework-env-prefixes?utm_source=qiita&utm_campaign=socialhat&utm_medium=social