コース目次 / 第4章
直す — フィールド単位の認可と、入力の許可リスト
BOLA・BFLA・mass assignmentの3つを、リゾルバごとの所有者/ロール確認と、入力フィールドの許可リストで直します。同じ攻撃で再検証し、introspectionを無効化する方法にも触れます。
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章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。