こんにちは!「つゔぁいちゃんねる(2ch.biz)」開発録、第50回です。
WebRTCを活用したP2P暗号化チャットは、前回の「IndexedDB非同期デッドロック解消」を経て、スマホでもPCでも待ち時間ゼロで爆速入室できるようになりました。
しかし、実際にユーザー同士で使い始められた直後、セキュリティエンジニアの読者から冷や汗の出るような指摘が届きました。
「招待URLをコピーしてチャットに入ったあと、 ブラウザのアドレスバーに合言葉が平文のまま残っています!
画面共有した時や、チャット中のURLをそのまま他の人に再共有した時に、 パスワードが全世界にだだ漏れになりますよ! 」
「ワンクリックで合言葉も自動入力されて入室できたら便利だろう」と良かれと思って実装していた親切機能が、重大なセキュリティホール(平文漏洩)になっていたのです。
今回は、 「URLに機密情報を含めてはならない」 というWebセキュリティの鉄則に立ち返り、アドレスバーおよび招待URLから &key= パラメータを完全撤去し、パスワードをセキュアにメモリ内保持する改修の全貌をお届けします。
1. 発覚した脆弱性:なぜURLにパスワードを入れてはいけないのか?
もともとの実装では、友達をP2Pチャットに招待しやすいよう、以下のような形式のURLを発行していました。
https://2ch.biz/#p2p-room=room_p08e200775f&key=secret1234
URLのハッシュフラグメント(# 以降)はHTTPリクエストヘッダー(Referer)として外部サーバーには送信されないため、「ハッシュなら安全だろう」と油断していました。
しかし、現実のクライアントサイド運用において、URLにパスワードを残すことは以下のような甚大な漏洩リスクを抱えていました。
URLに合言葉が残る4大リスク
- アドレスバーの再コピペ : チャット中に「この部屋のURLちょうだい」と言われた参加者が、アドレスバーをそのままコピーして送ってしまう。
- 画面共有・配信での露出 : DiscordやZoomの画面共有、YouTube生配信などでアドレスバーが映り込んだ瞬間にパスワードが流出する。
- ブラウザ履歴への記録 : 共有PCやネットカフェのブラウザ履歴に、合言葉付きのURLがそのまま残ってしまう。
- 肩透かし(ショルダーハック) : カフェやオフィスで背後から画面を覗き見られた際、アドレスバーに大きな文字でパスワードが見えてしまう。
2. アーキテクチャの改善方針:URLの純化と二段階認証
安全性を最優先にするため、以下の設計へ方針を抜本的に転換しました。
-
招待URLにはルームIDのみを含める :
- URLは
https://2ch.biz/#p2p-room=p08e200775fの形式とし、パスワードに関する情報は1文字たりとも含めない。
- URLは
-
入室後のアドレスバーもIDのみを維持 :
- パスワード付きで入室した場合でも、ブラウザのアドレスバー(
window.location.hash)を即座に#p2p-room=${roomId}へ正規化・置換する。
- パスワード付きで入室した場合でも、ブラウザのアドレスバー(
-
合言葉(暗号化キー)はメモリ内管理 :
- パスワードはJavaScriptインスタンスの変数(
this.password)の中だけで保持し、URLやローカルストレージなどの永続領域には一切平文で記録しない。
- パスワードはJavaScriptインスタンスの変数(
3. 実装の改修ポイント
① 招待URLコピー処理の修正
ホストや参加者が画面上の「📋 招待URLをコピー」ボタンを押した際に生成されるURLから、&key= を完全に撤去しました。
// p2p_chat.js
copyInviteUrl() {
if (!this.roomId) {
this.showToast('ルームに参加していません', 'error');
return;
}
// 改善前:
// const keyParam = this.password ? `&key=${encodeURIComponent(this.password)}` : '';
// const url = `${baseUrl}#p2p-room=${this.roomId}${keyParam}`;
// 改善後: パスワードは絶対にURLへ含めない!
const baseUrl = window.location.origin + window.location.pathname;
const url = `${baseUrl}#p2p-room=${this.roomId}`;
navigator.clipboard.writeText(url).then(() => {
this.showToast('招待URLをコピーしました!\n※合言葉が必要な部屋の場合は、合言葉を別途お伝えください', 'success');
}).catch(err => {
console.error('クリップボードコピー失敗:', err);
prompt('以下の招待URLをコピーしてください:', url);
});
}
② 入室時のアドレスバー書き換え処理
ユーザーが部屋に入室を完了したフック関数 onRoomEntered() において、アドレスバーのURLをルームID単体にサニタイズします。
// p2p_chat.js
onRoomEntered(roomId) {
this.roomId = roomId;
// アドレスバーのハッシュを #p2p-room=${roomId} のみに更新
// 万が一、古いブックマークなどで &key= が含まれていても即座に消去する
try {
const cleanHash = `#p2p-room=${this.roomId}`;
if (window.location.hash !== cleanHash) {
// history.replaceState を使用して履歴を汚さずにURLを置換
if (window.history && window.history.replaceState) {
window.history.replaceState(null, '', cleanHash);
} else {
window.location.hash = cleanHash;
}
}
} catch (e) {
console.warn('URLハッシュの更新に失敗:', e);
}
}
window.history.replaceState を利用することで、ブラウザの「戻る」ボタン履歴に過去のパラメータ付きURLを残すことなく、アドレスバーを綺麗な状態に上書き更新できます。
4. 自動テストによる検証
URLから &key= が確実に排除されているか、単体テストスクリプトを作成して検証しました。
// test_invite_url_clean.js
const assert = require('assert');
function generateInviteUrl(originPath, roomId, password) {
// 修正後のロジック: password引数は受け取ってもURLには一切結合しない
return `${originPath}#p2p-room=${roomId}`;
}
const testRoom = "room_p08e200775f";
const secretPass = "SuperSecretPassword1234!";
const url = generateInviteUrl("https://2ch.biz/", testRoom, secretPass);
console.log("生成されたURL:", url);
// 1. &key= が含まれていないこと
assert.strictEqual(url.includes("&key="), false, "エラー: URLに&key=が含まれています!");
// 2. パスワード平文がどこにも含まれていないこと
assert.strictEqual(url.includes(secretPass), false, "エラー: URLにパスワード平文が含まれています!");
// 3. ルームIDが正しく指定されていること
assert.strictEqual(url, `https://2ch.biz/#p2p-room=${testRoom}`, "エラー: ルームIDが一致しません");
console.log("✅ すべてのセキュリティアサーションに合格しました!");
$ node test_invite_url_clean.js
生成されたURL: https://2ch.biz/#p2p-room=room_p08e200775f
✅ すべてのセキュリティアサーションに合格しました!
5. まとめと次回予告
利便性を追い求めるあまり、「URLをクリックするだけで全員入室できたら楽だよね」と安易にパラメータへ含めてしまった合言葉。
しかし、URLは 「共有されること」「画面に映ること」「履歴に残ること」 を前提としたパブリックな識別子です。
秘密情報はURLから完全に排除し、安全なインメモリ管理へと移行したことで、ユーザーが安心して画面共有やURLの受け渡しを行えるようになりました。
さて、URLが綺麗になったところで、次に気になってきたのが 「ルームIDの見た目」 です。
現在のルームIDは room_p08e200775f のように room_ という接頭辞が付いています。
しかし、スマホの小さな画面でコピーしたり手入力したりする際、この room_ が地味に邪魔で、打ち間違いの原因にもなっていました。
「この room_ って、そもそも本当に必要だろうか……?」
次回は、 【第051回】room_プレフィックスは本当に必要?11文字英数字の純粋ID体系へ完全統一 をお届けします。お楽しみに!
本番サービス : https://2ch.biz/