テック技報最新号へ →
コード

TypeScript: 所有者チェック済みをブランド型で表し、IDOR を型で防ぐ

「所有者チェックを通った結果としてしか作れない型」を用意し、更新・削除などの処理はその型だけを受け取るようにする。チェックを書き忘れたコードはコンパイルが通らなくなる。

@yousukezan · 投稿 2026-10-12
#typescript#security#IDOR#認可#branded-type#hono

どんな問題か

IDOR(Insecure Direct Object Reference)は「ログインはしているが、そのデータに触ってよい人かを確認していない」バグ。GET /orders/:id や POST /orders/:id/cancel で WHERE id = $1 だけを使い、user_id の条件を書き忘れると、他人の注文が見えたりキャンセルできたりする。一覧 API には条件があるのに詳細 API だけ抜ける、という「書き漏れ」が典型。

効かなかった案

  • ミドルウェアでチェック: 分かるのは「認証済みか」まで。「この注文の所有者か」は担保できず、どのルートにかけるかも人が判断する。
  • サービスクラスに集約: 新しいメソッドを足した人がチェックを書き忘れれば同じ穴が開く。

型で表す(推奨)

ts
// owned-order.ts
declare const ownedOrderBrand: unique symbol
export type OwnedOrder = Order & { readonly [ownedOrderBrand]: true }

// OwnedOrder を作れるのはこの関数だけ
export async function findOwnedOrder(auth: AuthContext, orderId: number): Promise<OwnedOrder | null> {
  const { rows } = await pool.query(
    'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
    [orderId, auth.userId],
  )
  return rows[0] ? (rows[0] as OwnedOrder) : null
}

// order-service.ts — 引数が OwnedOrder なら所有者確認は済んでいる
export async function cancelOrder(order: OwnedOrder) { /* ... */ }

// ハンドラ(Hono の例)
const order = await findOwnedOrder(auth, Number(c.req.param('id')))
if (!order) return c.json({ error: 'not found' }, 404)
await cancelOrder(order)

設計のポイント

  • 証明に対象 ID を含める: userId だけを持つ「所有者証明」にすると、注文10で得た証明を注文99の操作に使い回せる。証明(OwnedOrder)そのものが対象の注文を表すようにし、操作関数に注文 ID を別引数で渡さない。
  • as OwnedOrder は1ファイルに閉じ込める: キャストできる場所が増えると偽造できる。ESLint(no-restricted-syntax など)でそのファイル以外の as OwnedOrder を禁止すると機械的に守れる。
  • 見つからない場合も他人のものの場合も同じ 404 にすると、ID の存在を漏らさない。

注意

  • ブランド型はコンパイル時だけの保証。実行時の検査は SQL の user_id 条件そのものなので、findOwnedOrder のクエリは必ずテストで確認する。
  • 管理者が他人のデータを扱う操作は、別のブランド型(例: AdminAccessibleOrder)を用意して経路を分ける。

出典

X の元の投稿を開く