「改行キーを1万回連打したレスが投稿され、他のユーザーのスマホブラウザがクラッシュした」
令和に自作した2ちゃんねる風掲示板「つゔぁいちゃんねる(2ch.biz)」開発秘話の第13弾。今回は、荒らしが好んで使う「長文コピペ爆弾」や「改行テロ」、名前欄のレイアウト破壊を物理的に防ぎ、データベースとクライアントのDOM描画を守り抜く入力制限の設計について解説します。
💡 はじめに:文字数制限だけでは防げない「改行テロ」の脅威
Web開発の入門書には必ず「フォームにはバリデーションをかけましょう」と書かれています。
「本文は4,000文字以内にしておけば大丈夫だろう」――そう考えていませんか?
実は、文字数制限だけでは掲示板の破壊を防ぐことはできません。
悪意ある荒らしは、文字数の隙間を突いて次のような攻撃を仕掛けてきます。
1. 画面崩壊・スクロール地獄を引き起こす「改行テロ」
本文欄に改行(\n)だけを1,000回連続で入力して投稿します。
文字数としてはたったの1,000文字(制限内)ですが、ブラウザで表示すると 縦に数万ピクセルもの虚無の空白領域 が出現し、他のユーザーは目的のレスを読むために延々とスクロールを強いられることになります。
2. 名前欄・タイトル欄での「改行インジェクション」
本来1行で収まるべき「名前」や「スレッドタイトル」に改行コードを無理やりねじ込み、テーブルの枠組みやヘッダーのレイアウトをぐちゃぐちゃに破壊する攻撃です。
3. メガバイト級テキスト爆弾によるDOMレンダリングフリーズ
青空文庫の小説全文や巨大なバイナリ文字列を一撃で投稿し、データベースのストレージを圧迫するだけでなく、そのスレッドを開いた一般ユーザーのブラウザメモリを食い潰してタブを強制クラッシュさせます。
この破壊工作に対抗するため、開発初期(2026年8月18日)に 「加重文字数 + 行数制限の多層バリデーション」 を実装しました。
📐 設計した制限仕様一覧
2ちゃんねるの伝統的な文化(長文語りやアスキーアート)の自由度を尊重しつつ、破壊行為だけを確実に切り落とす絶妙な閾値を設定しました。
| 対象項目 | 加重文字数制限 | 行数制限 | 設計意図・エラーメッセージ |
|---|---|---|---|
| 💬 レス・スレッド本文 | 全角 2,048文字 / 半角 4,096文字 (加重4,096) | 最大 50行 | AAやプログラミングコードの投稿を許容しつつ、改行テロを遮断。本文の行数が多すぎます(最大50行 / 現在: 52行)
|
| 🏷️ スレッドタイトル | 全角 140文字 / 半角 280文字 (加重280) | 1行のみ (改行不可) | X(旧Twitter)と同等の表現力を確保しつつ、見出しの折り返し崩れを防止。 |
| 👤 投稿者名(トリップ含む) | 全角 140文字 / 半角 280文字 (加重280) | 1行のみ (改行不可) | 長すぎるコテハンや改行によるヘッダー破壊を防止。 |
| 📇 メール欄(sage等) | 全角 140文字 / 半角 280文字 (加重280) | 1行のみ (改行不可) |
sage などのコマンド入力欄。改行や長文を排除。 |
🛠️ なぜ strlen ではなく「加重文字数(mb_strwidth)」なのか?
日本語のWebアプリケーションにおいて、「文字数」の数え方は極めて厄介です。
-
半角文字(
A,1,!) : 画面上の専有幅は狭い(1カラム)。 -
全角文字(
あ,漢,★) : 画面上の専有幅は広い(2カラム)。 -
絵文字・サロゲートペア(
👨👩👧👦,𠮷) : 内部的には4〜10バイト以上を消費する。
もしPHPの strlen()(バイト数)で数えると、日本語はUTF-8で1文字3バイト消費するため、全角文字を少し書いただけで「制限オーバーです」と怒られてしまいます。
逆に mb_strlen()(文字数)で数えると、全角文字も半角文字も同じ「1文字」と判定されるため、全角文字で画面が埋め尽くされてレイアウトが膨張してしまいます。
そこで、 「全角を2文字分、半角を1文字分として計算する East Asian Width(加重文字数)」 を採用しました。
// サーバーサイドでの加重文字数チェック(api.php)
function getWeightedLength($str) {
// mb_strwidth は半角を1、全角を2として数える
return mb_strwidth($str, 'UTF-8');
}
🚫 改行テロを断ち切る行数バリデーション
本文の改行コードを正規化し、行数をカウントして50行を超えていないかを厳密に検査します。
public function validatePostInput($name, $email, $content) {
// 1. 名前・メアドの改行混入チェック(1行厳守)
if (preg_match('/[\r\n]/', $name)) {
throw new InvalidArgumentException("名前欄に改行を含めることはできません。");
}
if (preg_match('/[\r\n]/', $email)) {
throw new InvalidArgumentException("連絡先・メール欄に改行を含めることはできません。");
}
// 2. 本文の改行コード統一と行数カウント
$normalizedContent = str_replace(["\r\n", "\r"], "\n", $content);
$lines = explode("\n", $normalizedContent);
$lineCount = count($lines);
$maxLines = 50;
if ($lineCount > $maxLines) {
throw new InvalidArgumentException("本文の行数が多すぎます(最大 {$maxLines} 行 / 現在: {$lineCount} 行)。");
}
// 3. 本文の加重文字数チェック(全角2048 / 半角4096)
$maxWeightedLength = 4096;
$currentLength = mb_strwidth($normalizedContent, 'UTF-8');
if ($currentLength > $maxWeightedLength) {
$maxZen = $maxWeightedLength / 2;
throw new InvalidArgumentException("本文は全角 {$maxZen} 文字(半角 {$maxWeightedLength} 文字)以内で入力してください。");
}
}
工夫したポイント:「現在何行オーバーしているか」を教える親切なUX
単に「行数が多すぎます」と突き放すのではなく、 「(最大50行 / 現在: 52行)」 と現在の行数をエラー文に含めるようにしました。
これにより、少しだけ行数をオーバーしてしまった一般ユーザーは、「あと2行削ればいいんだな」と瞬時に理解して修正することができます。
🛡️ クライアント&サーバーの多層防御アーキテクチャ
バリデーションは、サーバーサイドだけでなくクライアント側(HTML・JavaScript)でも二重・三重にガードを敷いています。
-
第1層:HTML属性(
maxlength)
<textarea maxlength="4096">や<input maxlength="280">を指定し、通常のタイピングではこれ以上入力できないようブラウザ側で物理制御。 -
第2層:JavaScript事前バリデーション
送信ボタンを押した瞬間、行数と文字数をクライアント側で計算。エラーがあればサーバーへリクエストを飛ばすことなく即座にフォーム下部へ案内を表示(サーバー負荷ゼロ)。 -
第3層:バックエンド最終防壁
curlやPostman、開発者ツールからの直接API呼び出しを想定し、サーバー側で例外を投げて完全に遮断。
🎯 まとめと次回予告
第13回となる今回は、掲示板の画面美とサーバーリソースを悪質なテキスト攻撃から守る 「加重文字数&改行数バリデーション」 について解説しました。
今回の学び・ポイント
- 改行テロには行数制限が必須 :文字数制限だけでは空白スクロールテロを防ぐことはできない。
-
加重文字数(
mb_strwidth)の公平性 :日本語全角と半角英数の占有幅の差を考慮したカウント設計。 - 現在値を示す親切なエラー表示 :数字を明示することで、一般ユーザーのストレスと離脱を防ぐ。
掲示板の入力フォームは盤石になりました。
しかし、攻撃者が狙うのは一般画面だけではありません。
「掲示板の心臓部である『管理者ログイン画面』へのパスワード総当たり(ブルートフォース)攻撃」 です。
次回、 【第14回】管理者ログインを守り抜け!パスワード試行制限と指数関数的バックオフによるブルートフォース防御 へと続きます!
🔗 関連リンク
- 本番サービス : https://2ch.biz/