Appearance
アプリケーション セキュリティ監査レポート
監査日: 2026-02-20 対象: BUSON アプリケーション全体(フロントエンド・バックエンド・データベース・インフラ)
概要
BUSONアプリケーションのセキュリティリスクを網羅的に調査した結果をまとめる。 既に対策済みのマイページ関連リスク(Referrer漏洩、レート制限、トークン有効期限等)は既存ドキュメントを参照のこと。
アーキテクチャ前提
| 項目 | 内容 |
|---|---|
| 認証方式 | Cloudflare Access(JWT ヘッダーベース、Cookie 認証ではない) |
| DB アクセス | バックエンドが service_role キーで Supabase に接続(RLS バイパス) |
| 公開ページ | /mypage, /hr-mypage は公開トークン(UUID v4)で認証 |
| 利用者 | 社内スタッフのみ(Cloudflare Access で制御) |
| ホスティング | フロントエンド: Cloudflare Pages / バックエンド: Cloud Run |
リスク一覧(危険度別)
危険度の定義
| 危険度 | 定義 |
|---|---|
| Critical | 悪用された場合、データ漏洩・改ざん・サービス停止が即座に発生する |
| High | セキュリティ境界を越えたアクセスが可能になる、または本番事故につながる |
| Medium | 特定条件下でリスクが顕在化する、または防御層が不足している |
| Low | 理論上のリスクだが実際の悪用は困難、または影響が限定的 |
| Info | ベストプラクティスとの乖離。直接的な脅威ではない |
サマリーテーブル
| # | リスク名 | 危険度 | カテゴリ | 対応推奨 |
|---|---|---|---|---|
| 1 | ファイルアップロードの MIME 検証不備 | Critical | バックエンド | 即対応 |
| 2 | IDOR — テナント所有権検証の欠如 | High | バックエンド | 要検討 |
| 3 | RBAC 未実装 | High | バックエンド | 要検討 |
| 4 | セキュリティヘッダーの不足 | High | インフラ | 即対応 |
| 5 | CSP(Content Security Policy)未設定 | High | フロントエンド | 即対応 |
| 6 | 開発環境での認証バイパス | Medium | バックエンド | 改善推奨 |
| 7 | 認証済みルートのレート制限なし | Medium | バックエンド | 改善推奨 |
| 8 | react-markdown の XSS 対策不足 | Medium | フロントエンド | 改善推奨 |
| 9 | エラーメッセージによる情報露出 | Medium | 両方 | 改善推奨 |
| 10 | ログへの PII 出力 | Medium | バックエンド | 改善推奨 |
| 11 | uploaded_by のハードコード | Medium | バックエンド | 改善推奨 |
| 12 | クローラー認証情報の管理 | Medium | インフラ | 中期対応 |
| 13 | キャッシュ制御ヘッダーの欠如 | Low | バックエンド | 任意 |
| 14 | iframe sandbox 属性の未設定 | Low | フロントエンド | 任意 |
| 15 | クエリパラメータの型バリデーション | Low | バックエンド | 任意 |
| 16 | API キー未設定時のフォールバック | Low | バックエンド | 任意 |
| 17 | npm audit の定期実行 | Info | 両方 | 継続 |
詳細
1. ファイルアップロードの MIME 検証不備
- 危険度: Critical
- 場所:
backend/src/interfaces/routes/candidates.ts(L392-432) - カテゴリ: バックエンド
現状:
typescript
const ALLOWED_MIME_TYPES = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
return c.json({...}, 400)
}file.type はクライアントが Content-Type で自由に設定できる。サーバー側でファイル内容(magic bytes)の検証を行っていない。
被害シナリオ:
- 攻撃者が実行可能ファイル(.exe)を
Content-Type: application/pdfで送信 - Supabase Storage に PDF として保存される
- 他のユーザーがダウンロードして実行するとマルウェア感染
対応推奨: magic bytes 検証を実装
typescript
function validateMagicBytes(buffer: ArrayBuffer, expectedType: string): boolean {
const bytes = new Uint8Array(buffer).slice(0, 4)
switch (expectedType) {
case 'application/pdf':
// PDF: %PDF (25 50 44 46)
return bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
// ZIP/OOXML: PK (50 4B 03 04)
return bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04
default:
return false
}
}2. IDOR — テナント所有権検証の欠如
- 危険度: High
- 場所:
backend/src/interfaces/routes/candidates.ts,companies.ts等、全リソースエンドポイント - カテゴリ: バックエンド
現状:
typescript
// 任意の ID でアクセス可能
candidatesRoutes.get('/:id', async (c) => {
const id = Number(c.req.param('id'))
const candidate = await candidateService.getById(id)
// ← 認証ユーザーと候補者の所属関係を検証していない
})ID は連番のため推測が容易。認証されたユーザーであれば、URL の ID を変えるだけで任意のレコードにアクセスできる。
被害シナリオ:
- スタッフ A が担当外の求職者の個人情報(給与、連絡先、面接メモ)を閲覧
- 企業の機密契約情報を別スタッフが参照
文脈と判断:
現在のユーザーは全員同一組織の社内スタッフであり、Cloudflare Access で外部アクセスは遮断されている。現時点では社内スタッフ間のデータ分離が業務上必須かどうかがポイント。
- 社内全員が全データにアクセスして問題ない場合 → 対応不要(現状維持)
- 担当者ごとにデータを分離したい場合 → RBAC と合わせて対応が必要
対応推奨: 業務要件に応じて判断。分離が必要なら agent_id ベースのフィルタリングを実装。
3. RBAC 未実装
- 危険度: High
- 場所:
backend/src/interfaces/middleware/auth.ts - カテゴリ: バックエンド
現状:
認証ミドルウェアはメールアドレスのみを取得。ロール(管理者/エージェント等)の区別がなく、全認証ユーザーが同じ操作権限を持つ。
被害シナリオ:
- 新人スタッフが誤って企業情報を一括削除
- 退職予定者が在籍中にデータを大量エクスポート
文脈と判断:
小規模チームでの運用が前提のため、現時点で RBAC が不要な場合もある。ただし組織拡大時にはリスクが増大する。
対応推奨: 中期的に users テーブルに role カラムを追加し、管理操作(削除等)に管理者ロールを要求。
4. セキュリティヘッダーの不足
- 危険度: High
- 場所:
frontend/public/_headers - カテゴリ: インフラ
現状:
/*
Referrer-Policy: no-referrerReferrer-Policy のみ設定済み。以下のヘッダーが欠落:
| ヘッダー | 用途 | 欠落時のリスク |
|---|---|---|
Strict-Transport-Security | HTTPS 強制 | MITM 攻撃 |
X-Content-Type-Options: nosniff | MIME スニッフィング防止 | ファイル型偽装攻撃 |
X-Frame-Options: DENY | クリックジャッキング防止 | iframe 埋め込みによる操作誘導 |
Permissions-Policy | ブラウザ API 制限 | カメラ・位置情報等の不正取得 |
対応推奨:
/*
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Permissions-Policy: camera=(), microphone=(), geolocation=()5. CSP(Content Security Policy)未設定
- 危険度: High
- 場所:
frontend/index.html,frontend/public/_headers - カテゴリ: フロントエンド
現状: CSP ヘッダーもメタタグも設定されていない。
被害シナリオ:
- XSS 脆弱性が発見された場合、攻撃者がインラインスクリプトや外部スクリプトを注入可能
- CSP がなければブラウザ側の防御層がゼロ
対応推奨: _headers ファイルに追加
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-src 'none'; object-src 'none'; base-uri 'self'注意
'unsafe-inline' は Tailwind CSS のインラインスタイルに必要。可能であれば nonce ベースに移行。
6. 開発環境での認証バイパス
- 危険度: Medium
- 場所:
backend/src/interfaces/middleware/auth.ts(L20-28) - カテゴリ: バックエンド
現状:
typescript
const isDevelopment = process.env.NODE_ENV !== 'production'
if (isDevelopment) {
c.set('user', { email: 'dev@example.com', name: 'Development User' })
return next()
}NODE_ENV が production 以外のすべての値で認証がスキップされる。
被害シナリオ: ステージング環境やテスト環境で NODE_ENV=staging のように設定すると、認証なしでAPIにアクセス可能。
対応推奨: 明示的な開発モードフラグの使用
typescript
const SKIP_AUTH = process.env.SKIP_AUTH === 'true' && process.env.NODE_ENV === 'development'7. 認証済みルートのレート制限なし
- 危険度: Medium
- 場所:
backend/src/interfaces/routes/全ルート - カテゴリ: バックエンド
現状: レート制限は公開スケジュール API (/public/) のみ実装。認証済みルート (/api/v1/candidates 等) には未実装。
被害シナリオ:
- 認証済みユーザーが GET リクエストを大量送信して DB に負荷
- スクリプトによる全データの自動エクスポート
対応推奨: グローバルレート制限ミドルウェアを追加(例: 認証ユーザーは 100req/min)
8. react-markdown の XSS 対策不足
- 危険度: Medium
- 場所:
frontend/src/components/candidate/AiSummaryBubble.tsx(L103),frontend/src/components/hr/HrAiSummaryBubble.tsx(L96) - カテゴリ: フロントエンド
現状:
tsx
<Markdown>{aiSummary || ''}</Markdown>Gemini API から返却された AI 要約をそのままレンダリング。react-markdown はデフォルトでサニタイズされるが、明示的な制限設定がない。
被害シナリオ: Gemini API のレスポンスが汚染された場合(プロンプトインジェクション等)、意図しない HTML がレンダリングされる可能性。
対応推奨:
tsx
<Markdown allowedElements={['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'h2', 'h3']}>
{aiSummary || ''}
</Markdown>9. エラーメッセージによる情報露出
- 危険度: Medium
- 場所: バックエンド:
backend/src/interfaces/middleware/error-handler.ts, フロントエンド:frontend/src/hooks/useCandidates.ts等 - カテゴリ: 両方
現状:
- バックエンド:
ValidationError時にバリデーション詳細メッセージを返す - フロントエンド:
error.messageをトースト通知にそのまま表示
typescript
// フロントエンド
toast.error('登録エラー', {
description: error.message || '求職者の登録に失敗しました',
})被害シナリオ: エラーメッセージからテーブル構造・カラム名・バリデーションルールが推測可能。
対応推奨:
- バックエンド: 500 エラーは汎用メッセージのみ返す(現状OK)。400 エラーのメッセージも定型文に統一を検討
- フロントエンド: HTTP ステータスコードに基づく定型メッセージに変換
10. ログへの PII 出力
- 危険度: Medium
- 場所:
backend/src/interfaces/middleware/masked-logger.ts - カテゴリ: バックエンド
現状: UUID のマスキングは実装済みだが、クエリパラメータ内の個人情報はマスクされない。
GET /api/v1/candidates?keyword=田中太郎&email=tanaka@example.com
→ ログに name と email が平文で出力対応推奨: クエリパラメータの keyword, email, phone 等をマスク対象に追加。
11. uploaded_by のハードコード
- 危険度: Medium
- 場所:
backend/src/interfaces/routes/candidates.ts(L462) - カテゴリ: バックエンド
現状:
typescript
uploaded_by: 1, // TODO: Get from auth contextファイルアップロード時の uploaded_by が常に 1 でハードコードされている。
被害シナリオ: 監査ログが信頼できない。誰がファイルをアップロードしたか追跡不能。
対応推奨: 認証コンテキストからユーザー ID を取得して設定。
12. クローラー認証情報の管理
- 危険度: Medium
- 場所:
backend/.env.local(L49-105) - カテゴリ: インフラ
現状: 13種以上のクローラー認証情報(メール、パスワード、セッション Cookie)が .env.local に平文保存。
良い点
.gitignoreに.env.localが含まれており、リポジトリにはコミットされない- GitHub Actions では
secretsを使用
被害シナリオ: 開発マシンが侵害された場合、外部サービス(HRMOS, AGRE等)の認証情報が漏洩。
対応推奨: 中長期的に HashiCorp Vault 等のシークレット管理サービスへの移行を検討。
13. キャッシュ制御ヘッダーの欠如
- 危険度: Low
- 場所:
backend/src/app.ts - カテゴリ: バックエンド
現状: API レスポンスに Cache-Control ヘッダーが設定されていない。
被害シナリオ: 求職者の個人情報がブラウザキャッシュや共有プロキシに保存される可能性。
対応推奨:
typescript
app.use('/api/*', async (c, next) => {
await next()
c.header('Cache-Control', 'no-store')
})14. iframe sandbox 属性の未設定
- 危険度: Low
- 場所:
frontend/src/components/PdfPreviewDialog.tsx(L91) - カテゴリ: フロントエンド
現状: PDF プレビュー用の <iframe> に sandbox 属性がない。
対応推奨:
tsx
<iframe src={doc.url} sandbox="allow-same-origin" referrerPolicy="no-referrer" />15. クエリパラメータの型バリデーション
- 危険度: Low
- 場所:
backend/src/interfaces/routes/candidates.ts(L46-69) 等 - カテゴリ: バックエンド
現状: Number(query.status_id) で型変換しているが、NaN のエラーハンドリングがない。Supabase の PostgREST が型安全性を担保するため実害は低い。
対応推奨: クエリパラメータにも Zod スキーマを適用するとベター。
16. API キー未設定時のフォールバック
- 危険度: Low
- 場所:
backend/src/interfaces/routes/jobs.ts(L28-32) - カテゴリ: バックエンド
現状:
typescript
_geminiClient = new GeminiClient({ apiKey: process.env.GEMINI_API_KEY || '' })API キーが未設定の場合、空文字でクライアントが初期化され、実行時に分かりにくいエラーになる。
対応推奨: 起動時にバリデーションして明確なエラーメッセージを出力。
17. npm audit の定期実行
- 危険度: Info
- 場所:
frontend/package.json,backend/package.json - カテゴリ: 両方
対応推奨: CI パイプラインに npm audit を組み込み、定期的に脆弱性を確認。
対策済み項目(既存ドキュメント)
以下のセキュリティ対策は既に実装済み。詳細は各ドキュメントを参照。
| 対策 | ドキュメント |
|---|---|
| 公開トークンの Referrer 漏洩対策 | リファラーリスク |
| 公開スケジュール API レート制限 | レート制限 |
| 公開トークン有効期限 | トークン有効期限 |
| 公開スケジュールのリスク分析 | 公開スケジュールリスク |
| 異常検知の Slack 通知 | Slack通知E2E |
| anon ロールの権限撤回 | マイグレーション 20260209014252_revoke_anon_access.sql |
| UUID マスキング(ログ) | backend/src/interfaces/middleware/masked-logger.ts |
| 公開 API 緊急遮断機能 | PUBLIC_SCHEDULE_MUTATIONS_DISABLED / PUBLIC_TOKEN_BLOCKLIST 環境変数 |
| Resend Webhook 署名検証 | backend/src/infrastructure/email/resendWebhookVerifier.ts |
| CORS 制限 | backend/src/app.ts — 環境変数 ALLOWED_ORIGINS で明示指定 |
補足: CSRF リスクの評価
本アプリケーションでは Cloudflare Access が CF-Access-JWT-Assertion ヘッダーを自動付与する方式で認証を行っている。これは Cookie ベース認証ではないため、従来の CSRF 攻撃(別サイトからのフォーム送信で Cookie が自動送信される)は 成立しにくい。
加えて以下の防御層がある:
- CORS 設定で許可オリジンを明示指定
- Cloudflare Access が認証ゲートウェイとして機能
このため、CSRF リスクは Low と評価する。
補足: RLS ポリシーの評価
RLS ポリシーが USING (true) で設定されている点について、実運用では以下の理由で直接的なリスクは限定的:
- バックエンドは
service_roleキーで接続するため、RLS は常にバイパスされる anonロールの権限は20260209014252_revoke_anon_access.sqlで完全に撤回済み- Supabase Auth(
authenticatedロール)を直接使用するクライアントは存在しない
ただし、防御の多層化(Defense in Depth) の観点からは、将来的に RLS ポリシーを適切に設定することが望ましい。
対応優先度
即対応(1-2 週間)
- [ ] ファイルアップロードの magic bytes 検証 (#1)
- [ ] セキュリティヘッダーの追加 (#4)
- [ ] CSP ヘッダーの設定 (#5)
短期対応(1 ヶ月以内)
- [ ] 認証バイパスの安全策 (#6)
- [ ] エラーメッセージのサニタイズ (#9)
- [ ] uploaded_by の修正 (#11)
- [ ] キャッシュ制御ヘッダー追加 (#13)
中期対応(要件に応じて)
- [ ] RBAC の実装検討 (#3)
- [ ] IDOR 対策の検討 (#2)
- [ ] 認証済みルートのレート制限 (#7)
- [ ] クローラー認証のシークレット管理移行 (#12)
継続的改善
- [ ] npm audit の CI 組み込み (#17)
- [ ] react-markdown の制限設定 (#8)
- [ ] ログ PII マスキング強化 (#10)