コース目次 / 第4章

直す — フィールド単位の認可と、入力の許可リスト

BOLA・BFLA・mass assignmentの3つを、リゾルバごとの所有者/ロール確認と、入力フィールドの許可リストで直します。同じ攻撃で再検証し、introspectionを無効化する方法にも触れます。

第4章 / 全6章目安 約12分この章のゴール: BOLA・BFLA・mass assignmentを直し、同じ攻撃で再検証できるようになる

3つの穴を、vuln-api.js に手を入れて直します。原則は、これまでと同じです——sec-securecoding の「事実はサーバが持つ」「許可リストで明示する」。

法と倫理ゲート

手を動かす前に

このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。

  • (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
  • (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
  • (c) CTF 競技の中(規約の範囲で)

実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。

直し1 — BOLA: 所有者を確認する

// 直したあと(BOLA): 所有者チェックを追加。
note: ({ id }) => {
  const n = notes[id];
  if (!n) throw new Error('見つかりません');
  if (n.owner !== uid) throw new Error('権限がありません');
  return n;
},

直し2 — BFLA: 呼び出し元のロールを確認する

// 直したあと(BFLA): 管理者ロールでなければ拒否。
deleteUser: ({ id }) => {
  if (users[uid].role !== 'admin') throw new Error('管理者権限が必要です');
  delete users[id];
  return true;
},

直し3 — mass assignment: 許可リストで、フィールドを選ぶ

// 直したあと(mass assignment): 更新してよいフィールドだけ、許可リストで適用。
updateProfile: ({ input }) => {
  const ALLOWED = ['name', 'bio'];
  for (const key of Object.keys(input)) {
    if (ALLOWED.includes(key)) users[uid][key] = input[key];
  }
  return users[uid];
},

Object.assign(users[uid], input) という「丸ごと代入」をやめ、「更新してよいと決めたフィールドだけ」を、1つずつ確認して適用します。input にどんなフィールドが混ざっていても、許可リストに無いものは無視されます。これは、スキーマの型定義とは別に、アプリケーション側で持つべき許可リストです——「スキーマで定義されている=クライアントが自由に設定してよい」ではない、という区別が大事です。

再検証 — 3つの攻撃を、もう一度

サーバを再起動して、前章までの攻撃をすべて繰り返します。

# BOLA
curl -s -X POST -H "Content-Type: application/json" -H "X-UID: 1" \
  -d '{"query":"{ note(id: 102) { id owner text } }"}' http://127.0.0.1:4200/
# => {"errors":[{"message":"権限がありません", ...}], "data":{"note":null}}

# BFLA
curl -s -X POST -H "Content-Type: application/json" -H "X-UID: 1" \
  -d '{"query":"mutation { deleteUser(id: 2) }"}' http://127.0.0.1:4200/
# => {"errors":[{"message":"管理者権限が必要です", ...}], "data":{"deleteUser":null}}

# mass assignment
curl -s -X POST -H "Content-Type: application/json" -H "X-UID: 1" \
  -d '{"query":"mutation { updateProfile(input: {name: \"架空太郎2\", role: \"admin\"}) { id name role } }"}' \
  http://127.0.0.1:4200/
# => {"data":{"updateProfile":{"id":1,"name":"架空太郎2","role":"user"}}}

3つとも、意図どおりに塞がりました。名前の更新(name)は通り、role だけは無視されて “user” のままです。念のため、正規の操作(note(id: 101)、自分のメモ)はちゃんと通ることも確かめてください。

introspection も、本番では絞る

第1章の introspection は、直接「悪用」する穴というより、偵察を助けてしまう情報開示でした。多くの GraphQL サーバフレームワーク(Apollo Server など)には、本番環境で introspection を無効にする設定があります。この教材の最小実装では手作業での分岐が要りますが、実務では「開発環境では有効、本番では無効」を環境変数などで切り替えるのが定石です。

無効にしても、URLや大まかな機能は他の経路(フロントエンドのコードなど)から推測されうるので、introspectionを閉じることは「絶対の壁」ではなく「偵察を難しくする一枚」です。本命は、あくまで各リゾルバの認可チェックです。

持ち帰る一言

事実(所有者・ロール)はサーバが確認し、入力は許可リストで受け取る。 GraphQL のリゾルバも、REST のハンドラも、直しの原則は同じでした。次はまとめて、API診断の視点をまとめます。

こうなっていればOK

卒業まであと1章です。

この章はまだ完了していません。