2001年10月5日。「1ch.tv」という掲示板サービスが鳴り物入りで産声を上げました。

1ch.tv/amezo/index.htmlより引用, 2001年10月5日閲覧
当時インターネット接続の普及にともなって利用者が爆増していた、「2ちゃんねる」。それら当時「治安の悪い」と見做されがちだった匿名型掲示板のありかたに一石を投じるべく「人にやさしい掲示板」を標榜し、さらに次のような構想を掲げていました。
- 実名・ペンネーム・匿名を使い分けられるが、別名義の使い分けによるいわゆる「自作自演」はできない
- 各掲示板には編集長や編集者を置き、掲示板の品質を担保する
- 半数の掲示板では有料サービスを実施し、情報発信者が1アクセス5~10円程度の課金を設定できる
- 3GケータイやPHSを利用したリアルタイムの映像・音声・記事の有料発信ができる
- 日本語を十数ヶ国語に自動翻訳できる
これらの先進的な機能により、1ch.tvは「知価創造化社会」の実現を目指すと宣言していました。
言うまでもなく、これらは21世紀最初の四半世紀で次々登場した「ブログ」、「SNS」、そして「YouTube」などで全て実現してインターネットの基幹プラットフォームとなっています。これらのサービス需要を的確に予見していた1ch.tvの先見性には、目を見張るものがあります。
しかし実際には、そうはなりませんでした。史実の1ch.tvは「反2ch」の徒花として2chのネットウォッチ板を短期間賑わし、次々と信じられないようなセキュリティ事故やネットトラブルを巻き起こし、すっかりキワモノと見做されて短期間で注目を失い、以後は知る人ぞ知る存在になっていきました。
あまりに、もったいない。一体、何故なのか。
本稿では1ch.tv崩壊の序曲となった「Apache再インストール&ソースコード流出事件」までの時期にスポットを当て、技術的観点から当時のスクリプトの脆弱性を詳細に解説します。
おことわり
本稿は「1ch.tv」という歴史的Webサイトを題材としてインターネットにおけるリテラシー向上およびセキュリティ啓発を図ることを目的としたコンテンツです。
取り扱っている掲示板は既にサービスが廃止されてから長い年月が経過しており攻撃に悪用される可能性が現時点では低いこと、また1ch.tv側の公開設定(パーミッション等)の結果として、スクリプトが一時的に第三者から参照可能な状態になっていたことから、これを著作権法に基づき必要最小限の範囲で引用して紹介します。
1. 背景
スレッドフロート型匿名掲示板「2ちゃんねる」(以下、「2ch」)が巨大化する過程では、その荒さや危うさを内包しつつも、匿名性と放任的統治によって成立していた側面があります。
一方、1ch.tvはそんな2chのありかたに異を唱え、後年のブログやSNSなどのネットサービスへ発展し得る思想を先取りしようとしていたように見受けられます。
しかしそんな崇高な理念は、技術・運用・統治の三拍子が揃って初めて成立するものです。
- 技術: 技術力の高いアーキテクトとエンジニア
- 運用: 公正なアドミニストレーターとオペレーター
- 統治: 思想と倫理を備えたマネジメントとプロデューサー
1ch.tvの崩壊が短期間で加速したのはそれら全てが未成熟であったこと、要するに「その器でなかった」ことが原因であったと考えます。
本稿ではこのうち「技術」の側面にスポットを当てて、1ch.tv崩壊の原因をご紹介します。
2. 1ch.tv掲示板機能解析
まずは1ch.tvの掲示板システムとしての機能を、流出したスクリプトから読み解きます。
2.1 データモデル
2.1.1 投稿ログ
1ch.tv の掲示板は当時の2chと同様に RDBMS を用いず、スレッドごとに1ファイルを作成し、1行=1投稿(レス)として追記していく方式を採用していました。
行の区切りは改行(\n)、カラム区切りには文字列 <> が用いられています。
2.1.2 投稿ログ生成
bbs.cgi では、次のように投稿データを生成しています。
#textに内容出力/REMOTE_HOSTは簡易記録
$host=' ';
#$value .=$FORM{'kaomoji'};#顔文字機能はいまはない。
$name= substr($FORM{'postname'},0,40);
#スタッフのキャップ実験
if($FORM{'postmail'} =~/^$staff/){
$name = "<font color=red>$name</font>";
$FORM{'postmail'} = "";
}
$name= "$name".'<font size=2 color=#000000>さん</font>';
$mail= substr($FORM{'postmail'},0,50);
#$name= $autoname if ($FORM{'postname'} eq '');#名無し名を付ける
&error("名前を入れて下さい<hr> $value") if ($FORM{'postname'} eq '');
$mail =~s|(https?://[-\w.!~*'();/?:@&=+$,%#]+)||g;
$logs_url= "$FORM{'logs_url'}";
$text = "$msg<>$name<>$mail<>$date<>$host<>$value<>";
$text.="<><><>$logs_url<><><><>";
$text =~ s/\n//g;
$text .="\n";
#httpから始まるものをリンクに変換
$text =~s|(https?://[-\w.!~*'();/?:@&=+$,%#]+)|<a href="$+" target=_blank>$+</a>|g;
#read.cgiヘのレス番号指定のための変換&アスキーアートや行数制限
$text =~ s/>>([0-9]+)\-([0-9]+)/<a href=$read_cgi?fol=$folder&log=$log&s=$1&e=$2 target=_blank>>>$1\-$2<\/a>/g;
$text =~ s/>>([0-9]+)(?![-\d])/<a href=$read_cgi?fol=$folder&log=$log&s=$1&e=$1 target=_blank>>>$1<\/a>/g;
@a = ($text =~ /<br>/g);
(@a + 0 > 30 || $text =~ /──/) && &error("コメントが長すぎます <hr> $value");
この $text が、そのままスレッドのファイルへ書き込まれます。
2.1.3 投稿ログ解析
同じく bbs.cgi では、ログ行を以下のように解析しています。
#######################
#書き込みをすべて表示
#(書きこみを全部表示するHTMLファイル作成が主。ついてにチャット用HTMLファイルを作成)
#######################
sub alllist{
#ぜんぶ見るファイルを開く
$all_log_html = "$pre_dir".'log_html/'."all_log"."$log".'.html';
open(ALL_LOG,"+>$all_log_html");
eval 'flock(ALL_LOG,2);';
select ALL_LOG;
#スレッドにタイトルを入れる処置
if(-e"$ti_dir$log"){
open(TITLE,"$ti_dir$log");
@titles = <TITLE>;
close(TITLE);
}
($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url0,$dummy)
=split(/<>/,$titles[0]);
print <<EOL;
<html><head><title>$msg</title></head>
<body bgcolor=#$thcolor text=#$textcolor link=#$linkcolor vlink=#$vlinkcolor
marginwidth="10" marginheight="10" topmargin="10" leftmargin="10">
<a href="../index.html" >掲示板に戻る</a></center>
EOL
このコードから、少なくとも先頭10カラムは何らかの意味を持つことがわかります。
さらに末尾には、未使用の空カラムがさらに3つほど存在するようです。
2.1.4 スレッドログ(log/ 配下)のデータ構造
スレッドフロート型掲示板としてのオーソドックスな「スレ立て」「レス」のみが行われた状態のプレーンなデータは、次のようになっていたと考えられます。
スレタイ<>名無しさん<>sage<>01/10/05 01:34:56<> <>
これは本文です<> <> <>
<> <> <>
<>アナザー名無しさん<> <>01/10/05 01:40:12<> <>
それな<> <> <>
<> <> <>
レス行では $msg(スレッドタイトル)が空になります。
なおカラム上は $host が用意されていますが、実際は空文字相当で固定されており、投稿ログ上はホスト情報が記録されない実装になっています。
ホスト情報は下記のコードにより別ファイルに一括して保存されていたようです。
################
#ipのログ取得
################
sub ipget{
undef @ip_list;
my $saidai_suu=50000;###最新表示数
my $ip_file="../../ppp/1chip";
open(IP_LOG,"$ip_file");@ip_list=<IP_LOG>;close(IP_LOG);
####最大表示数を越えたら、越えたものを削除する
@ip_list=@ip_list[0..$saidai_suu-1] if($#ip_list >$saidai_suu);
my $ip_data="$FORM{'postname'}"."$folder ##IP##"."$ENV{'REMOTE_ADDR'}"."##IP##"."$ENV{'REMOTE_HOST'}"."$date";
unshift @ip_list,$ip_data."\n";
open(IP_LOG,"+>$ip_file");
eval 'flock(IP_LOG,2);';print IP_LOG @ip_list ;eval 'flock(IP_LOG,8);';close(IP_LOG);
REMOTE_HOST は逆引き設定等に依存するため、常に有効な値が得られるとは限りませんが、少なくとも運用者側ではIP関連情報を別ファイルに集約していたことが分かります。
2.1.5 「○」評価
1ch.tvではそれぞれの投稿に「○」ボタンがついています。これを押すと表示されている数字がカウントアップされ、投稿の評価が数字で定量化・可視化されるというわけです。
そう。これは歴史的にかなりの早期に実装された実質「いいね」機能といえます。
「○」評価コード
#############################
####書き込みに対する判定つまり○の投票
#############################
sub judg{
$log=$FORM{'logfile'};
$file = "$log_dir"."$log";
#ターゲットファイル読み込み
open(DB,"$file");@taget=<DB>;close(DB);
#ターゲットの行削除
$gyou = splice(@taget,$FORM{'res'}-1,1);
#1行を分解します
($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url,$dummy)
=split(/<>/,$gyou);
@judg_ip=split(/#/,$judg);
foreach(@judg_ip){
&error('投票は一人一回です') if( $ENV{'REMOTE_ADDR'} eq $_);
}
($FORM{'good'}) && (push @judg_ip,$ENV{'REMOTE_ADDR'});
$judg = join("#",@judg_ip);
#元に戻します
$gyou=join("<>",($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url,$dummy));
chomp($gyou);
splice(@taget,$FORM{'res'}-1,0,$gyou."\n");
#カキコファイル書き込み
@stat = stat( "$file" );
open(DB,"+>$file");
eval 'flock(DB,2);';
print DB @taget;
eval 'flock(DB,8);';
close(DB);
#投票ではスレッドをあげない。
$now = time;
utime ($now, $stat[9], "$file" );
}
「○」評価データ構造
<>アナザー名無しさん<> <>01/10/05 01:40:12<> <>
それな<>203.0.113.10#203.0.113.22#198.51.100.7<> <>
<> <> <>
ボタンを押したユーザーのIPアドレスを記録し、多重投票を阻止していたようです。
なお当時は現在と違って常時接続のインターネット環境は少なく、切断および再接続によってIPアドレスを変えることは現在よりもずっとハードルが低いものでした。しかし、一定の効果はあったと思われます。
なおサービス開始当初の10月時点では「×」ボタンも存在しており、3回押されると投稿が削除される(おそらく実際には非表示化)という機能が備わっていましたが、程なくして廃止されています。
これは後のインターネットの歴史において各サービスで散見された「低評価ボタン」の機能や意義の紆余曲折を先取りしていたと言えるでしょう。
実装としてはおそらく、使用されていない $judg2 に同様に#で区切られたIPアドレス列が入っていたと推測できます。
「評価の可視化」を投稿単位で実装していた点は、後年のSNSが標準化した設計に近い発想です。
2.1.6 一言レス
1ch.tvでは通常のレスの他に、レスにさらにぶら下げる「一言レス」という機能がありました。スレッド型掲示板の基本構造の、いわば2段目までのネスト化です。
一言レスコード
####################
###1行レスの処理
####################
sub small_res{
$log=$FORM{'logfile'};
$file = "$log_dir"."$log";
#ターゲットファイル読み込み
open(DB,"$file");@taget=<DB>;close(DB);
#ターゲットの行削除
$gyou = splice(@taget,$FORM{'res'}-1,1);
#1行を分解します
($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url,$dummy)
=split(/<>/,$gyou);
$FORM{'small_res'} =~s|(https?://[-\w.!~*'();/?:@&=+$,%#]+)|<a href="$+">$+</a>|g;
############################################
#削除名を含んでいないならそのまま投稿
#含んでいたら処理しない
if(crypt($FORM{'small_res'},'am')!~/$delname/o){
@small_res =split(/<#>/,$small_res);
$samll_res_num=$#small_res+2;
$new_smallres = $FORM{'small_res'}.' '."$date1"."<br>";
###################2重投稿チェック
(length($small_res[-1]) eq length($new_smallres)) && &error('2重投稿エラー');
push @small_res,$new_smallres;
###################10行以上は書きこめない
($#small_res >4) && (@small_res = @small_res[-5..-1]);
$small_res = join("<#>",@small_res);
###################元に戻します
$gyou=join("<>",($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url,$dummy));
chomp($gyou);
splice(@taget,$FORM{'res'}-1,0,$gyou."\n");
}
一言レスデータ構造
<>アナザー名無しさん<> <>01/10/05 01:40:12<> <>
それな<>203.0.113.10#203.0.113.22#198.51.100.7<> <>
一言レスです 01/10/05 01:50<br><#>一言レスだぜ 01/10/05 01:55<br>
<> <> <>
いまの言葉で言えば投稿スレッドの上に「短文リアクション」を積む設計で、SNS的な体験を志向していたと捉えられます。
2.2 bbs.cgi の掲示板識別処理
bbs.cgi は 1ch.tv における掲示板機能の中核を担う CGI であり、単なる「書き込み」に留まらず、掲示板で生じるほぼ全ての状態変更操作を一手に引き受けています。
下記の処理はすべて bbs.cgi 内に実装されています。
- 投稿本文の保存(スレッドログへの追記・上書き)
- スレッド一覧(インデックス)の生成
- 全ログ HTML の生成(
log_html/配下) - スレッド全体・個別レスの削除処理
- 「○」評価(judg:投票者 IP の蓄積と重複判定)
- 一言レス(small_res:ログ行の再構築を伴う追記)
掲示板のあらゆる機能を「一つの CGI ファイルに詰め込む」設計で、何か機能を追加するたびにコード量が増大し、結果として安全性と保守性が低下するという構造的な弱みがあります。
bbs.cgi 冒頭の対象掲示板特定コード
#############################################
#リファラチェック&$folderに変数を入れる
$referer ="$ENV{'HTTP_REFERER'}";
$referer =~ /^$urlbase([\w\d]+)/ || &error("エラー [1ch.tv]からアクセスしていますか?");
$folder = $1;#対象掲示板フォルダ名(/は入っていません)
#飛んでくる前のデレクトリ
$pre_dir='../'.$folder.'/';
#書き込み用ログファイルのディレクトリ
$log_dir='../'.$folder.'/log/';
#タイトルリスト作成用ログファイルディレクトリ
$ti_dir='../'.$folder.'/title/';
#$pre_dirでもいい気がするけど念のため
$p_dir ='../'.$folder.'/';
このコードは、bbs.cgi が「どの掲示板を対象として操作するか」を決定する、極めて重要な初期処理です。処理内容を分解すると、以下のとおりになります。
-
HTTP_REFERERヘッダの取得
クライアント(ブラウザ)が送信した参照元 URL を$refererとして取得する -
参照元 URL の形式チェックと板名抽出
$refererが$urlbase + 英数字列という形式に一致するかを正規表現で確認し、
一致した場合、その英数字部分のみを$folderとして取り出す -
$folderを起点としたディレクトリパスの組み立て
抽出した$folderをそのまま相対パスに結合し、以降のファイル操作先を決定する
$folder からのディレクトリ決定
$folder の値が確定した瞬間に、bbs.cgi が読み書きするファイル群のルートが連動して確定します。
-
../$folder/
掲示板のルートディレクトリ。設定ファイル(bbs.ini等)が置かれる -
../$folder/log/
スレッド本文ログ。1スレッド=1ファイル、1行=1投稿という形式で管理される -
../$folder/title/
タイトル用ログ。スレッド一覧生成の補助として利用される -
../$folder/log_html/
検索結果や過去ログとして生成される HTML ファイル群
さらに後段には、書き込みフォーム form.cgi からアクセスされた場合のみ $folder をフォーム入力値で上書きする処理が実装されています。
#フォームからデータ取得
&getform;
################################################
#read_imode.cgi/read.cgi/form.cgiからの投稿の場合
#リファラで掲示板ディレクトリを設定できないため
#FORMからディレクトリを取得
if($folder eq 'cgi'){
$folder=$FORM{'folder'};
$pre_dir='../'.$folder.'/';
$log_dir='../'.$folder.'/log/';
$ti_dir='../'.$folder.'/title/';
$p_dir ='../'.$folder.'/';
}
これは掲示板識別子の決定ロジックが一貫していないことを示しています。
これらの結果として具体的に何が起こり得るのかについては、後の章でパストラバーサルや権限不在の削除処理とあわせて詳述します。
2.3 read.cgi の掲示板・スレッド識別処理
read.cgi は、スレッド閲覧用の CGI です。2chにおけるそれと、機能は同じです。
役割は一見シンプルで、指定されたスレッドログ(log/ 配下の1ファイル)を読み込み、HTMLとして整形してブラウザへ返します。
機能としては下記のようになります。
- 板(掲示板)とスレッド(ログファイル)の特定
-
bbs.iniの読み込み(表示色やタイトルなど、板固有の見た目・挙動を反映) - 表示範囲の制御(開始・終了・最新n件などの指定)
- ログ行の分解(
<>区切り)と HTML への組み立て - レス番号の採番・アンカーリンク等の UI 生成
そして read.cgi もまた「板境界の決め方」と「ファイルパスの作り方」が、1ch.tvの安全性と運用安定性を左右する構造になっています。
read.cgi パス組み立て
#read_imode.cgi/read.cgi/form.cgiからの投稿の場合
#リファラで掲示板ディレクトリを設定できないため
#FORMからディレクトリを取得
$folder=$FORM{'fol'}; #read.cgiだけ$FORM{'fol'}です。他のCGIはFORM{'folder'}なおしゃいいんだけど
$pre_dir='../'.$folder.'/';
$log_dir='../'.$folder.'/log/';
$ti_dir='../'.$folder.'/title/';
$p_dir ='../'.$folder.'/';
$log=$FORM{'log'};
if(-e"$log_dir$log") {
open(DB,"$log_dir$log");
@lines = <DB>;
close(DB);
}
このコードでは「どの掲示板のどのスレッドを読むか」を決め、以降の処理で参照するディレクトリ・設定ファイルの場所を確定させています。
入力パラメータから板とスレッドをリクエストパラメータから受け取る
-
$FORM{'fol'}
操作対象の掲示板ディレクトリ名 -
$FORM{'log'}
読み込むスレッドログファイル名
$folder から参照ディレクトリを確定
-
$pre_dir = ../$folder/
掲示板ルート -
$log_dir = ../$folder/log/
スレッドログ置き場
$log_dir と $FORM{'log'} からログファイルを確定
-
$log_dir$log
ログファイル
これが具体的にどのような問題へ発展し得るかは、次の章で詳しく解説します。
3. 1ch.tv掲示板システムの脆弱性
ここでは1ch.tvスクリプトから読み取れる、実際の具体的な脆弱性を説明します。
CSRFなど2001年当時まだ一般化の途上だったセキュリティ観点は割愛し、あくまで当時の水準に照らしてすら「脆弱」だったと言える箇所についてのみ言及します。
3.1 保存先ディレクトリをリファラから取ってしまっている
先述した、 bbs.cgi の保存先ディレクトリ決定ロジックを再掲します。
#############################################
#リファラチェック&$folderに変数を入れる
$referer ="$ENV{'HTTP_REFERER'}";
$referer =~ /^$urlbase([\w\d]+)/ || &error("エラー [1ch.tv]からアクセスしていますか?");
$folder = $1;#対象掲示板フォルダ名(/は入っていません)
#飛んでくる前のデレクトリ
$pre_dir='../'.$folder.'/';
#書き込み用ログファイルのディレクトリ
$log_dir='../'.$folder.'/log/';
#タイトルリスト作成用ログファイルディレクトリ
$ti_dir='../'.$folder.'/title/';
#$pre_dirでもいい気がするけど念のため
$p_dir ='../'.$folder.'/';
#フォームからデータ取得
&getform;
################################################
#read_imode.cgi/read.cgi/form.cgiからの投稿の場合
#リファラで掲示板ディレクトリを設定できないため
#FORMからディレクトリを取得
if($folder eq 'cgi'){
$folder=$FORM{'folder'};
$pre_dir='../'.$folder.'/';
$log_dir='../'.$folder.'/log/';
$ti_dir='../'.$folder.'/title/';
$p_dir ='../'.$folder.'/';
}
技術的な危険性
この設計は一見分かりやすいのですが、セキュリティおよび運用の観点では複数の問題を内包しています。
-
そもそも
HTTP_REFERERを信頼してはならない
リファラー(アクセス元ページ)はブラウザが付加的に送信する情報であり、クライアントサイドでの偽装が容易です。
本来、操作対象の掲示板はリクエスト URL やサーバ側のルーティングで決定すべきであって、 HTTP ヘッダに依存するのは不適切です。 -
識別子が改変されると、ファイル操作先も連動して改変される
$folderに任意の値を入れられるということは、../$folder/log/等の参照先も連動して変更できるということです。
bbs.cgiは新規追加や上書き、削除といった強い操作権限を持つため、意図しない漏洩や改竄のリスクが高まります。 -
攻撃でなくとも運用事故を誘発する設計
この問題は「攻撃者が悪意を持って操作した場合」に限りません。
URL 設計変更、リバースプロキシ導入などの運用上の変更でも、ファイルシステム上の境界が崩壊し事故に繋がるリスクを常に孕んでいます。
改善策
- 掲示板の識別子はURLパスの固定ルーティングで決定し、ヘッダに依存しない
-
folderのような“パス要素”は 英数字限定+長さ制限+存在確認(ホワイトリスト) - 可能ならDBで板IDを管理し、ファイルパスとは分離する
3.2 パストラバーサル・IDOR
先述した、 read.cgi のログファイルパス決定ロジックを再掲します。
#read_imode.cgi/read.cgi/form.cgiからの投稿の場合
#リファラで掲示板ディレクトリを設定できないため
#FORMからディレクトリを取得
$folder=$FORM{'fol'}; #read.cgiだけ$FORM{'fol'}です。他のCGIはFORM{'folder'}なおしゃいいんだけど
$pre_dir='../'.$folder.'/';
$log_dir='../'.$folder.'/log/';
$ti_dir='../'.$folder.'/title/';
$p_dir ='../'.$folder.'/';
$log=$FORM{'log'};
if(-e"$log_dir$log") {
open(DB,"$log_dir$log");
@lines = <DB>;
close(DB);
}
さらに、 form.cgi の同様の処理についても見てみます。
&getform;
$folder=$FORM{'folder'};
#飛んでくる前のデレクトリ
$pre_dir='../'.$folder.'/';
#書き込み用ログファイル
$log_dir='../'.$folder.'/log/';
#タイトルリスト作成用
$ti_dir='../'.$folder.'/title/';
#$pre_dirでもいい気がするけど念のため
$p_dir ='../'.$folder.'/';
############################
#記事を引用する $line に出力
############################
sub kiji{
$kijinum = $_[0];
#引用もとの記事を表示するためにログを開く
$log = $log_dir.$FORM{'logfile'};
open(DB,"$log");@lines = <DB>;close(DB);
($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url0,$dummy)
=split(/<>/,$lines[$kijinum - 1]);
技術的な危険性
この実装の危険性は、下記の2点です。
-
IDOR(Insecure Direct Object Reference)
folとlogを知っていれば、その板・そのスレッドを直接指定して閲覧できる構造です。
本来アクセス制御が必要な掲示板(内部連絡用、スタッフ用等)が存在した場合、URL(パラメータ)を推測できるだけで閲覧できてしまう危険性があります。 -
パストラバーサル
入力値がそのまま'../'.$folder.'/log/'.$logの形でパス結合されます。結合点が$folderと$logの二箇所あるため、どちらか一方でも相対パス要素や想定外の文字列が混入すれば、ディレクトリ外参照に発展し得ます。
加えて、-eによる存在確認は「安全性の検証」ではなく「参照可能性の確認」に過ぎず、入力検証が欠けている限り根本対策にはなりません。
つまり、read.cgi は実装上、条件次第で「スレッド閲覧」ではなく「サーバ上のファイル内容を断片的に取り出す」挙動に近づき得ます。
確証はありませんが、2001年12月6日未明のソースコード流出以前に発生していた不可解な攻撃を説明しうる要因ではあると考えます。
改善策
-
fol/logはサーバ側で発番したID(UUID等)にし、ファイルパスとは分離する - ログはDocumentRoot外へ置き、Web側から直接参照できない構造にする(当時の専用ブラウザ文化との相性は課題になりますが)
3.3 管理者権限の認証・認可の不全
bbs.cgi には、「削除モード」という機能が存在しました。
bbs.cgi: 削除モード
###############名前が削除用の名前なら削除モードに突入します。
if(crypt($FORM{'postname'},'am') eq $delname){&del1;$value='';}
bbs.cgi: bbs.ini から $delname 読み込み
#初期設定をbbs.iniから読みこむ
$inifile = "$pre_dir"."bbs.ini";
if( !open (INI, $inifile )) {
print "Content-type: text/html\n\n";
print $inifile;
&error ( "設定ファイルが開きません。管理者に連絡してください。" );
exit;
}
foreach $pair (<INI>) {
($name, $value) = split(/=/, $pair);
$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
&jcode'convert(*value,'sjis');
&jcode'h2z_sjis(*value);
$value =~ s/\n//g;
$form{$name} = $value;
}
close(INI);
#bbs.iniから読みこんだ変数を入れる
# (略)
$delname = $form{'delname'}; #削除用の名前
bbs.cgi: 削除処理
##########################
####削除用のサブルーチン(スレッド全部)
##########################
sub del1{
if($FORM{'postmail'} eq 'all') {
$log=$FORM{'logfile'};
unlink "$log_dir"."$log";
unlink "$ti_dir"."$log";
}else{
&del2;
}
}
#########################################
#削除用のサブルーチン2特定のカキコだけ(&del1内部)
#########################################
sub del2{
$log=$FORM{'logfile'};
$file = $log_dir.$log;
#ターゲットファイル読み込み
open(DB,$file);@target=<DB>;close(DB);
if($FORM{'value'} eq '' && $FORM{'postmail'}) {
$delnum = ','.$FORM{'postmail'}.',';
if($delnum =~ s/(\d+)\-(\d+)/join(',',($1..$2))/eg){
for($k=$2;$k>=$1;$k--){
#ターゲットの行削除
$sakujyo = splice(@target,$k-1,1);
}
}else{
$sakujyo = splice(@target,$FORM{'postmail'}-1,1);
}
#カキコファイル書き込み
open(DB,">$file");
print DB @target;
close(DB);
}elsif($value && $FORM{'postmail'}){
#一言レス削除
($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url,$dummy)
=split(/<>/,@target[$FORM{'postmail'}-1]);
@small_res =split(/<#>/,$small_res);
splice(@small_res,$value,1);
$small_res = join("<#>",@small_res);
@target[$FORM{'postmail'}-1]=join("<>",($msg,$name,$mail,$date,$host,$value,$judg,$judg2,$small_res,$logs_url,$dummy));
#カキコファイル書き込み
open(DB,">$file");
print DB @target;
close(DB);
}
}#del2_end
技術的な危険性
削除権限が「正規の認証セッション」ではなく、postname(ユーザー入力)に依存しています。crypt() はここでは「秘密の共有に基づく認証」になっておらず、入力値(postname)だけで強権限操作に到達できる点が致命的です(誰がどのスレッド・レスを削除できるか、という認可モデルが存在しない)。
またこの実装では誰が削除したかが曖昧で、削除行為が監査できません。ログ破壊や証拠の喪失は掲示板の運用上、大問題です。削除が濫用されると利用者は疑心暗鬼となり、それはコミュニティ崩壊へ直結します。
実際、1ch.tvではサービス開始初日のうちに bbs.ini が漏洩(というより普通に閲覧できようになっていたようです)し、削除用 postname のハッシュ値も漏洩していました。

1ch.tv/cgi/read.cgi?fol=info&log=11051018255 より引用(2001年10月5日閲覧)
ハードコーディングされているソルト 'am' も既知となっていますので、当時の Perl の古い crypt 関数が生成する DES ベースのハッシュ値は、(ハッシュ値が得られ、オフラインでの探索が可能な状況では)短いパスワードに対して総当たり探索のリスクが現実化し得ます。
さらに「認証が行われていない」という脆弱性は、「管理フォーム漏洩」という事態に対してもそのリスクを顕在化させました。

1ch.tv/cgi/read.cgi?fol=info&log=11051018255 より引用(2001年10月5日閲覧)
サービス開始当初、何故か掲示板作成画面が流出してしまい、殺到した2ch住人が野放図に「掲示板を新規作成」するという事態が発生しました。

1ch.tv/cgi/make.cgi より引用(2001年10月5日閲覧)
しかもこのスクリプト、流出範囲外のため今となっては詳細不明ですが、どうやら「掲示板タイトル名」では適切なサニタイジング(エスケープ処理)が行われておらず、結果として「<img src="http://~~~~/~~~/~~~.jpg">」のような文字列でさえ、そのまま素通りさせていました。
結果として当時は「エッチな画像が表示されている」程度のイタズラで済みましたが、今から考えれば悪意あるコードを埋め込んでのフィッシング詐欺や、マルウェア感染などのリスクが非常に高い状態でした。
改善策
- 管理操作は認証(AuthN)+認可(AuthZ)+監査ログが必須
- 削除は論理削除(フラグ)+復元手段が基本
- 削除権限は最小化し、責任主体を明確にする
3.4 格納型XSS
1ch.tv には、不思議な入力処理が存在していました。投稿フォームで@<@ を入力すると < に、@>@ を入力すると > に復元されるという仕様です。
一見すると「この仕様を知っている1ch.tv運営側が HTML を書き込むための裏技」にも見えますが、実装を精査するとこれは単なるサニタイズ不足ではなく、サニタイズ処理そのものを内部から破壊する設計であることが分かります。
bbs.cgi: getform
##############################
#FORM獲得
##############################
sub getform {
$buffer = $ENV{'QUERY_STRING'};
if ($ENV{'REQUEST_METHOD'} eq "POST") {
read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
}
@pairs = split(/&/,$buffer);
foreach $temp (@pairs) {
($name, $value) = split(/=/, $temp);
$value =~ tr/+/ /;
$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
&jcode_convert(*value,'sjis');
#タグと改行コードの整理
$value =~ s/>/>/g;$value =~ s/</</g;
$value =~ s/\t/ /g;$value =~ s/\r/\n/g;
$value =~ s/@>@/>/g;$value =~ s/@<@/</g;
$value =~ s/\n\n/\n/g;$value =~ s/\n/<br>/g;
$value =~ s/\"/"/g;#スタイルシート処理
$value =~ s/style//ig;#スタイルシート処理
$FORM{$name} = $value;
}
}
技術的な危険性
この実装の最大の問題点は、危険な HTML が「表示時」ではなく「保存前」に復活することです。
- 投稿内容はこの
getformを通過した後、スレッドログにそのまま保存される -
read.cgiやsearch.cgiはそのログを分解し、HTML としてユーザーに送信する
そのため、一度悪意ある HTML が混入すると、
- 悪意あるデータが永続化される(=格納型XSS)
- スレッド表示・一覧・検索・過去ログへと波及的に悪用される
というリスクが高まります。
実際、2001年12月10日には1ch.tv運営スタッフの一人をネタにした下記の動画(原ファイルは Flash swf)が1ch.tv上の掲示板に「投稿」されました。
当時の実際の投稿内容が参照できないため推定になりますが、(当時一般的だった Flash 埋め込み慣行と、コードから読み取れる復元仕様に照らすと) <embed> タグを使用して外部アップローダの swf ファイルを埋め込んだもので、ソースコード流出により上記の仕様がコード上から読み取れること、またエスケープ対象外のシングルクォーテーションを利用したものと推測できます。
改善策
- 原則としてユーザー入力を HTML として扱わない
- 正規表現による置換で HTML を安全化しようとしない
- 装飾を許可する場合は、
- 許可タグ・属性を限定し
- HTML パーサーによるホワイトリスト方式を用いる
余談: なぜ「style」に過剰反応しているのか
前述のコードには「スタイルシート処理」とコメントされた処理があり、それぞれ " を " に、 style を大文字小文字、出現回数にかかわらず削除する処理が追加されています。
ダブルクォーテーションはともかく、名前にもメールにも本文にも「STYLE」「Style」「style」いずれも書けない掲示板なんてちょっと不便な気がします。一体どうして、こんなことになってしまっているのでしょうか。
それは2001年10月5日未明、1ch.tvに2ch住人が大挙して押し寄せた狂乱の幕開けに遡ります。
<a href="mailto:" style="font-size:191pt">名無しさん@お腹いっぱい。
ご覧の通り、メールアドレスに「" style="font-size:191pt」を指定し、それがそのままデータとして保持され、表示された結果の悲劇です。当時は「いたずら」として扱われましたが、ユーザー入力がHTML文脈に侵入できる時点で、セキュリティ上は深刻です。
この事件が1ch.tv運営に強烈な印象を与えたのでしょう。 " と style は滅されることになりました。そもそも本稿では置換によるサニタイジングを推奨していませんが、この際にせめて ' や & もエスケープしなかったのは致命的だと考えます。
4. まとめ
1ch.tvは、理念としては当時の掲示板文化への強い問題意識から高い理想を掲げ、後年のブログやSNS、そしてYouTubeへと発展し得るポテンシャルを持っていました。
しかし、その夢は実現しませんでした。技術も、運用も、統治も、あまりにも稚拙だったからです。
流出した掲示板ソースからは入力境界・権限設計・出力処理が体系化されておらず、検索や削除のようなコア機能が脆弱になりやすい構造であったことが読み取れます。
さらにサーバー運用(HTTP設定・再インストール)での事故が重なり、ソースコードや(管理者の自作自演を証明する)IPアドレスの流出といった破局的な結末へと至りました。
運用や統治については、もはや語るまでもありません。
掲げた理念が高いほど、上記の要件は厳しくなります。1ch.tvはその意味で、インターネットにおける重要な失敗事例といえるでしょう。
参考文献
-
1ch.tv - Wikipedia(閲覧: 2026-01-25)
-
ASCII.jp(2001-08-12)「“2ちゃんねる”には欠陥がある!」西和彦アスキー特別顧問――“2ちゃんねる西スレッドオフ会”開催(閲覧: 2026-01-25)
-
ASCII.jp(2001-10-05)西和彦氏、“2ちゃんねる”へ宣戦布告!! ――掲示板群“1ch.tv”試験サービス開始!!(閲覧: 2026-01-25)
-
ASCII.jp(2001-10-23)掲示板群“1ch.tv”にDoS攻撃――警察は被害届を受理、本格捜査へ(閲覧: 2026-01-25)
-
INTERNET Watch(2001-10-04)西和彦氏、“人にやさしい”掲示板「1ch.tv」をプロデュース(閲覧: 2026-01-25)



