コース目次 / 第1章
Introspection — 地図そのものが、漏れる
自作GraphQLサーバに__schemaクエリを送り、全フィールド・全ミューテーションが丸見えになる様子を確認します。introspectionがなぜ攻撃の下見に使われるかを理解します。
GraphQL には、「このAPIが持つ、すべての型・フィールド・引数を問い合わせる」という標準機能があります——introspection(内省)です。開発中はとても便利ですが、本番でそのまま開けていると、攻撃者に完全な地図を渡すことになります。
法と倫理ゲート
手を動かす前に
このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。
- (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
- (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
- (c) CTF 競技の中(規約の範囲で)
実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。
標的を立てる
vuln-api.js として保存します。ユーザーとメモを持つ、小さな GraphQL API です。
// vuln-api.js — 学習用GraphQL API。127.0.0.1:4200。X-UID ヘッダで「いまの利用者」を表す。
const http = require('node:http');
const { graphql, buildSchema } = require('graphql');
const schema = buildSchema(`
type Query {
me: User
note(id: Int!): Note
}
type Mutation {
updateProfile(input: ProfileInput!): User
deleteUser(id: Int!): Boolean
}
input ProfileInput { name: String, bio: String, role: String }
type User { id: Int, name: String, role: String }
type Note { id: Int, owner: Int, text: String }
`);
const users = { 1: { id: 1, name: '架空太郎', role: 'user' }, 2: { id: 2, name: '架空花子', role: 'user' } };
const notes = { 101: { id: 101, owner: 1, text: '太郎のメモ' }, 102: { id: 102, owner: 2, text: '花子の非公開メモ' } };
function rootFor(uid) {
return {
me: () => users[uid],
note: ({ id }) => notes[id], // 【穴: BOLA。第2章で扱う】
updateProfile: ({ input }) => { Object.assign(users[uid], input); return users[uid]; }, // 【穴: mass assignment。第3章】
deleteUser: ({ id }) => { delete users[id]; return true; }, // 【穴: BFLA。第3章】
};
}
http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', async () => {
const uid = Number(req.headers['x-uid'] || 1);
const { query, variables } = JSON.parse(body);
const result = await graphql({ schema, source: query, rootValue: rootFor(uid), variableValues: variables });
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(result));
});
}).listen(4200, '127.0.0.1', () => console.log('やられGraphQL API: http://127.0.0.1:4200 (Ctrl+C で停止)'));
node vuln-api.js
正規の使い方
自分の情報を問い合わせます。
curl -s -X POST -H "Content-Type: application/json" \
-d '{"query":"{ me { id name role } }"}' http://127.0.0.1:4200/
{"data":{"me":{"id":1,"name":"架空太郎","role":"user"}}}
攻撃 — スキーマを、丸ごと問い合わせる
__schema という特殊なフィールドに問い合わせると、API の全体構造が返ってきます。
curl -s -X POST -H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name fields { name } } } }"}' \
http://127.0.0.1:4200/
返ってくる JSON には、Query 型の全フィールド(me、note)だけでなく、Mutation 型の updateProfile や deleteUser まで、丸ごと並びます。画面のどこにも「削除ボタン」が無くても、このクエリ一発で「deleteUser というミューテーションが存在する」ことが攻撃者に知られます。sec-authz の「フォースブラウジング」(隠しURLを推測して叩く)を、GraphQL は推測すら要らず、聞けば教えてくれる形にしてしまいます。
なぜ、これが偵察になるのか
攻撃の第一歩は、いつも「何があるか知る」ことでした(sec-report の偵察を思い出してください)。introspection は、その偵察を1回の問い合わせで終わらせてしまいます。型名・フィールド名・引数の型・必須かどうかまで、すべて分かる。
これは、REST の API ドキュメントを公開しているのと同じことです——開発中は便利で、社内向けなら問題ないこともありますが、公開する本番 API では、意図せず内部構造を晒すことになりがちです。多くの GraphQL サーバフレームワークは、本番環境向けに introspection を無効化するオプションを用意しています。
持ち帰る一言
introspectionは、地図そのものを渡す。 __schema クエリ1本で、隠しておきたいミューテーションの存在まで含めた全体構造が分かってしまう。本番では基本的に無効化する対象です。次は、この地図で見つけた note フィールドを、実際に攻撃します——BOLA です。
こうなっていればOK
卒業まであと4章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。