Skip to content

アプリケーション セキュリティ監査レポート

監査日: 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バックエンド即対応
2IDOR — テナント所有権検証の欠如Highバックエンド要検討
3RBAC 未実装Highバックエンド要検討
4セキュリティヘッダーの不足Highインフラ即対応
5CSP(Content Security Policy)未設定Highフロントエンド即対応
6開発環境での認証バイパスMediumバックエンド改善推奨
7認証済みルートのレート制限なしMediumバックエンド改善推奨
8react-markdown の XSS 対策不足Mediumフロントエンド改善推奨
9エラーメッセージによる情報露出Medium両方改善推奨
10ログへの PII 出力Mediumバックエンド改善推奨
11uploaded_by のハードコードMediumバックエンド改善推奨
12クローラー認証情報の管理Mediumインフラ中期対応
13キャッシュ制御ヘッダーの欠如Lowバックエンド任意
14iframe sandbox 属性の未設定Lowフロントエンド任意
15クエリパラメータの型バリデーションLowバックエンド任意
16API キー未設定時のフォールバックLowバックエンド任意
17npm 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)の検証を行っていない。

被害シナリオ:

  1. 攻撃者が実行可能ファイル(.exe)を Content-Type: application/pdf で送信
  2. Supabase Storage に PDF として保存される
  3. 他のユーザーがダウンロードして実行するとマルウェア感染

対応推奨: 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-referrer

Referrer-Policy のみ設定済み。以下のヘッダーが欠落:

ヘッダー用途欠落時のリスク
Strict-Transport-SecurityHTTPS 強制MITM 攻撃
X-Content-Type-Options: nosniffMIME スニッフィング防止ファイル型偽装攻撃
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_ENVproduction 以外のすべての値で認証がスキップされる。

被害シナリオ: ステージング環境やテスト環境で 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) で設定されている点について、実運用では以下の理由で直接的なリスクは限定的:

  1. バックエンドは service_role キーで接続するため、RLS は常にバイパスされる
  2. anon ロールの権限は 20260209014252_revoke_anon_access.sql で完全に撤回済み
  3. 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)