0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第030回】P2Pシグナリングサーバーを守れ!お問い合わせフォームとWebSocket接続へのBOT対策

0
Posted at

こんにちは!2ちゃんねる互換掲示板システム「 つゔぁいちゃんねる(2ch.biz) 」の開発録、第30回です。

第10回〜第14回で、私たちは掲示板の書き込みフォームに対して徹底的なセキュリティ対策(トークンバケット式Rate Limit、ハニーポット、海外IP遮断、ブルートフォース防御)を施しました。掲示板本体の荒らし対策は万全に見えました。

しかし、サービスを運用していると、攻撃者は常に 「対策の薄い裏口」 を探し出して突いてきます。

今回その標的となったのが、新設された 「お問い合わせフォーム」 と、第28回で導入した 「P2P暗号化チャットのシグナリングAPI」 でした。本稿では、この盲点となっていた2大エンドポイントをボットの魔の手から守るために講じたセキュリティ防壁の全貌を解説します。


1. 攻撃者が目をつけた「2つの裏口」

セキュリティを強化したシステムにおいて、攻撃者は最も防御の甘い場所へシフトします。

① お問い合わせフォームへのスパム爆撃

第20回でフッターにお問い合わせモーダルを設置したところ、海外の自動クローラーボットが検知し、怪しい宣伝リンクやフィッシングURLを含んだスパム問い合わせが1日に数百件単位で届くようになりました。

② P2PシグナリングAPIへの部屋乱発攻撃(DoS)

第28回で「サーバーにはログを残さない」P2Pチャットを公開しましたが、参加者同士をマッチングするためのシグナリングAPI(/api/p2p/join や /api/p2p/signal)はサーバー上で稼働しています。
ボットが自動スクリプトで数万件の幽霊ルームを乱発したり、無効なSDPシグナルを連続送信することで、シグナリングサーバーのインメモリマップを圧迫し、サービス停止(DoS)を狙う攻撃が発生しました。


2. 対策①:お問い合わせフォームへのハニーポット&BAN IP防壁

不可視の罠:CSSハニーポットの実装

人間には一切見えず、HTMLコードを走査して自動入力するボットだけが値を入れてしまうダミー入力欄(hp_website)をお問い合わせモーダル内に配置しました。

<!-- index.html: お問い合わせモーダル -->
<form id="contact-form">
    <div class="form-group">
        <label for="contact-name">お名前 / ハンドル</label>
        <input type="text" id="contact-name" name="name" required>
    </div>
    <div class="form-group">
        <label for="contact-email">メールアドレス</label>
        <input type="email" id="contact-email" name="email" required>
    </div>

    <!-- ★ボット捕獲用ハニーポット(人間には絶対に見えない) -->
    <div class="hp-trap-container" aria-hidden="true" style="opacity: 0; position: absolute; top: -9999px; left: -9999px; height: 0; width: 0; z-index: -1;">
        <label for="contact-hp-field">Leave this field empty</label>
        <input type="text" id="contact-hp-field" name="hp_website" tabindex="-1" autocomplete="off">
    </div>

    <div class="form-group">
        <label for="contact-message">お問い合わせ内容</label>
        <textarea id="contact-message" name="message" rows="5" required></textarea>
    </div>
    <button type="submit" class="btn btn-primary">送信する</button>
</form>

[!NOTE]
display: none だけを指定すると、一部の高度なクローラーボットが「非表示フィールド」と認識して入力をスキップする場合があります。そのため、opacity: 0; position: absolute; top: -9999px; を組み合わせ、ブラウザレンダリングエンジン上は存在しつつ視覚的に完全に画面外へ飛ばす手法を採用しています。

バックエンド側での即時検知・遮断

// お問い合わせ受信ハンドラ
app.post('/api/contact', (req, res) => {
    const clientIp = getClientIp(req);

    // 1. 掲示板本体の規制IPリスト(BANリスト)と照合
    if (isIpBanned(clientIp)) {
        return res.status(403).json({ error: 'このIPアドレスからのアクセスは制限されています。' });
    }

    // 2. ハニーポット検証
    const { name, email, message, hp_website } = req.body;
    if (hp_website && hp_website.trim().length > 0) {
        console.warn(`[BOT DETECTED] お問い合わせハニーポット作動: IP=${clientIp}`);
        // ボットには成功したように見せかけるか、400 Bad Request を返す
        return res.status(400).json({ error: '不正なリクエストが検出されました。' });
    }

    // 正常な問い合わせの保存・管理者通知処理
    saveContactMessage({ name, email, message, ip: clientIp });
    res.json({ success: true, message: 'お問い合わせを受け付けました。' });
});

3. 対策②:P2PシグナリングAPIへのボットトラップと規制連動

P2Pチャットのロビー画面(入室・部屋作成画面)にも同様にハニーポットトラップを仕掛けました。

// p2p_chat.js: シグナリングリクエスト送信部
async function sendSignal(action, payload) {
    // フォーム内のハニーポット値を取得
    const hpField = document.getElementById('p2p-hp-field');
    const hpValue = hpField ? hpField.value : '';

    const bodyData = {
        action: action,
        ...payload,
        hp_website: hpValue // ハニーポット値をサーバーへ送信
    };

    const res = await fetch('/api/p2p/signal', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(bodyData)
    });

    if (res.status === 403) {
        throw new Error('アクセスが禁止されているIPアドレスです。');
    }
    if (!res.ok) {
        throw new Error('シグナリング要求が拒否されました。');
    }
    return await res.json();
}

サーバー側では、受信したシグナリングリクエストに hp_website の値が入っていた場合、部屋の作成やピア登録を一切行わずに 400 Bad Request で遮断します。これにより、自動スクリプトによる幽霊部屋の大量生成攻撃を根絶しました。


4. 対策③:P2Pチャット内でのミリ秒連投・スパム荒らし防止

P2P接続が確立した後のチャットルーム内では、サーバーを介さないためサーバー側でのレートリミットが効きません。
そこで、クライアント側で 二重のスロットリング制御 を実装しました。

// クライアント側メッセージ送信レート制限
let lastMessageSentTime = 0;
let lastMessageContent = '';

function sendMessage() {
    const textarea = document.getElementById('p2p-message-input');
    const content = textarea.value.trim();
    if (!content) return;

    const now = Date.now();

    // 1. 500ミリ秒(0.5秒)未満の超高速連投を防止
    if (now - lastMessageSentTime < 500) {
        showChatNotice('⚠️ メッセージの送信間隔が早すぎます。少し待ってから送信してください。');
        return;
    }

    // 2. 3秒以内の同一メッセージ連続送信を防止(コピペ荒らし対策)
    if (content === lastMessageContent && now - lastMessageSentTime < 3000) {
        showChatNotice('⚠️ 同一のメッセージを連続で送信することはできません。');
        return;
    }

    // 送信時刻と内容を記録
    lastMessageSentTime = now;
    lastMessageContent = content;

    // 暗号化して全ピアへ送信
    broadcastEncryptedMessage(content);
    textarea.value = '';
}

5. 自動テストによる検証結果

改修後、自動検証スクリプトを作成し、正常なユーザー操作とボットによる攻撃シミュレーションを実行しました。

テスト項目 送信パラメータ 期待するステータス 判定
通常のお問い合わせ 正常値, hp_website: "" 200 OK (送信成功) ✅ PASS
BOTによるお問い合わせ スパム文, hp_website: "http://bot-promo.xyz" 400 Bad Request (自動遮断) ✅ PASS
規制IPからのお問い合わせ BAN済みIPからのリクエスト 403 Forbidden (即時ブロック) ✅ PASS
通常のP2Pルーム作成 正常値, hp_website: "" 200 OK (部屋作成成功) ✅ PASS
BOTによるP2Pシグナリング スクリプト自動送信, hp_website: "evil" 400 Bad Request (無効化) ✅ PASS
P2Pチャット内0.2秒連投 200ms間隔でのEnterキー連打 送信抑止 & トースト警告 ✅ PASS

6. まとめと次回予告

どんなに強固な城壁を築いても、裏口が開いていればシステムは簡単に侵入・破壊されてしまいます。
掲示板本体だけでなく、お問い合わせフォームやP2PシグナリングAPI、チャットルーム内のクライアント挙動にまでボット対策を張り巡らせることで、堅牢な運用体制が完成しました。

しかし、P2Pチャットを運用していく中で、次に直面したのが 「後から入室したユーザーの孤立問題」 でした。

中央サーバーにログがないため、会話の途中から入ってきたユーザーは真っ白な画面からスタートすることになり、 「今なんの話をしてるのか全く分からない!」 という事態に陥ってしまいます。

サーバーを介さずに、ピア同士が自律的に過去ログを融通し合うにはどうすればよいのか?

次回は、 【第031回】途中参加でも会話についていける!ピア間直接通信によるチャット過去ログの差分同期 をお届けします。お楽しみに!

本番サービス : https://2ch.biz/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?