コース目次 / 第2章
原因に、対処する
隠しエンドポイントを削除し、roleパラメータの代わりにサーバー側のトークン対応表で権限を判定するように修正します。それぞれの修正が、なぜ有効かを理解します。
穴を1つずつ、症状ではなく原因に対処します。
穴1 — デバッグ用エンドポイントを、削除する
コメントアウトや、パスを推測しにくい名前に変える——これらは「隠す」対策であり、直っていません。そもそも使われていないルートは、コードから削除します。存在しないパスは、どんなに頑張っても探しようがありません。
穴2 — roleの出どころを、変える
元のコードは、url.searchParams.get(‘role’)——クライアントが送ってきた値をそのまま信じていました。「あなたは誰か」という事実は、サーバーが持つべきです。ここでは簡易的に、Authorization: Bearer <トークン>ヘッダーと、サーバー側だけが持つトークン→ロールの対応表で判定するように変えます(sec-authで見たセッション管理の最小版です)。
修正版を、書く
arena-web-fixed.js として保存します。
const http = require('node:http');
const PAGE = `<!doctype html><meta charset="utf-8"><body>
<h1>Arena Gallery</h1>
<p>ようこそ。画像一覧は /api/photos から取得しています。</p>
</body>`;
// サーバーが持つ、トークンとロールの対応表(クライアントには見せない)
const SESSIONS = {
'guest-token-abc': 'guest',
'admin-token-xyz': 'admin',
};
http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:4500');
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (url.pathname === '/') { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.end(PAGE); return; }
if (url.pathname === '/api/photos') {
const auth = req.headers['authorization'] || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
const role = SESSIONS[token] || 'guest'; // 不明なトークンはguest扱い、パラメータは一切見ない
if (role === 'admin') {
res.end(JSON.stringify({ photos: ['public1.jpg', 'public2.jpg'], flag: 'flag{role_now_comes_from_the_server}' }));
return;
}
res.end(JSON.stringify({ photos: ['public1.jpg', 'public2.jpg'] }));
return;
}
res.writeHead(404); res.end(JSON.stringify({ error: 'not found' }));
}).listen(4500, '127.0.0.1', () => console.log('arena-web (fixed): http://127.0.0.1:4500 (Ctrl+C で停止)'));
差分は3つです。① /api/internal/debug-statusのルートそのものを削除、② HTMLからTODOコメント付きのscriptタグを削除、③ roleクエリパラメータの代わりにAuthorizationヘッダーとサーバー側の対応表で判定。「隠す」のではなく「入口を無くす」「信頼の起点をサーバーに移す」という、原因への対処です。
前章のサーバーをCtrl+Cで止め、修正版を起動します。
node arena-web-fixed.js
次の章で、同じexploitコマンドを試します。
こうなっていればOK
卒業まであと2章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。