3月に フリー写真素材 :: Free.Stocker という無料写真素材サイトをオープンし、このサイトのサイドバーに クックパッド みたいに気軽にメッセージを送れるフォームを設置しました。
しばらくは何事も無かったのですが、ある日突然大量のSPAMメッセージが送られてくるように…(●´⌓`●)
酷い時には3分〜5分に1通送られてくるし、試しに送り元のIPアドレスとホストを調べてみたら毎回バラバラなので困りました。
こんな時、多くのサイトでは CAPTCHA と呼ばれる読みづらい英数字の羅列の画像を入力させることが多いと思いますが、それでは「気軽にメッセージを送れるフォーム」とは言いづらいですし、サーバーにもページを表示させるたびに毎回余計な負担がかかってしまいます。
試しにWordPressのSPAM防止などに広く使われている Akismet というSPAM防止APIをメール送信スクリプトに組み込んでみましたが、今回はフォームの項目が1つしかなく、メッセージにURLが含まれないためかうまくフィルタリングしてくれません。
そこで、「JavaScriptがオンになっているブラウザからでないと正常にメッセージが送れないようにしてみてはどうだろう?」と思いました。
おそらく、多くのSPAM送信botは、JavaScriptを適切に解釈してメッセージを送信する機能はなく、単にHTMLの<form>タグ部分だけを解析してメッセージを送っているのではないかと思ったからです。
ちなみに、米Yahoo!によると JavaScriptをオフにしている人は1%前後 らしいので、メッセージが送れなくて困るという方はほとんどいないと思われます。
実装方法
HTML
1 2 3 4 5 | <form action="sendmail.php" method="post"> <input type="hidden" name="test" value="" id="formtest" /> <textarea cols="20" rows="5" name="message"></textarea> <input type="submit" value="送信" /> </form> |
1行目の action で指定されている sendmail.php はフォームの内容を受け取ってメールを送信するスクリプトです。
これは市販のメール送信スクリプトなら何でもかまいませんが、後で中身をちょっと書き換える必要があります。
ポイントは2行目の隠し要素。
name は test で value は空(から)になっています。
このまま送信されるとメール送信スクリプト(この場合は sendmail.php)側で弾いてしまい、この隠し要素の value に「特定の値が入っていた場合のみ」正常にメールが送信されるようにすれば良いのです。
次の jQueryスクリプトを、上記のHTMLより後ろ(</body>の直前など)に記述します。
JavaScript
1 2 3 4 5 | <script type="text/javascript"> $(function(){ $("#formtest").val("7hs4sk2"); }); </script> |
3行目以外はお約束なので実質1行ですね。
3行目は何をしているかというと、「HTMLの中から formtest というidが付いている要素を探し、その要素の value を 7hs4sk2 にしなさいということです。
この 7hs4sk2 は単なるランダムな文字列で、フォームの情報を受け取った sendmail.php は送られてきた情報の中に 7hs4sk2 があればメールを送信し、なければメールを送信しなければ良いわけです。
先ほど述べたようにメール送信スクリプトは何でも良いのですが、私がよく使っているメール送信スクリプト(PHP形式)の例を載せておきます。
PHP(sendmail.php)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> </head> <body> <?php mb_language("uni"); mb_internal_encoding("UTF-8"); //内部文字コードを変更 mb_http_input("auto"); mb_http_output("UTF-8"); //フォームの値を取得 if ($_SERVER["REQUEST_METHOD"] == "POST") { foreach($_POST as $k => $v){ //「magic_quotes_gpc=On」の時はエスケープ解除 if (get_magic_quotes_gpc()) { $v = stripslashes($v); } $v = htmlspecialchars($v); $$k = $v; } } else { exit(); } if($test != "7hs4sk2"){ exit("恐れ入りますが、ブラウザの設定(ツール>オプション)で「JavaScriptをオン」にしてから再度お試しください。"); } // IPアドレス・hostの取得 $ip = getenv("REMOTE_ADDR"); $host = getenv("REMOTE_HOST"); if ($host == null || $host == $ip) $host = gethostbyaddr($ip); //送信先メールアドレス $to = "mail@example.com"; // メール本文を組み立てます。 $naiyou = "$message IP: $ip HOST: $host"; if($message == "") { $h1 = "エラー"; $msg = "内容が書かれていません。ブラウザの戻るボタンで戻り、内容をお書きください。"; } else { if (mb_send_mail($to, "[メッセージ] Webサイトのフォームから", $naiyou)) { $h1 = "メッセージを送信しました"; $msg = "メッセージを頂き、誠にありがとうございました。<br /><br /><div style=\"text-align:center;\" align=\"center\"><a href=\"/\">トップページに戻る</a></div>"; } } // メッセージ echo "<h1>" . $h1 . "</h1>"; echo $msg; ?> </body> </html> |
ちょっと長いですが、PHPがよく分からない方でも14行目の送り先メールアドレス部分を書き換えてFTPでサーバにアップするだけですぐ使えます。
※私はプログラマーではなくセキュリティにはあまり詳しくないので、このスクリプトにはセキュリティホールがある可能性もあります。
もしこれらのスクリプトの使用により損害が生じた場合でも一切の保証は致しかねます。
セキュリティに詳しい方で、もしこれらのスクリプトにセキュリティホールを見つけた方はご連絡頂けると幸いです。
ポイントは30〜32行目のif文で、先程のHTMLの隠し項目 test の値が 7hs4sk2 でなければPHPを終了してエラーメッセージを表示し、メールを送信しないようになっています。
この部分さえ組み込めれば、どんなメール送信スクリプトでもこの方法を応用できるはずです。
ちなみに、このメール送信スクリプトは項目の追加が簡単で、例えばメールアドレスも入力して欲しければHTMLの3行目と4行目の間に
HTML
3 | <input type="text" name="mail" value="" /> |
を追加し、PHPの42行目に
PHP
42 | Mail: $mail |
を追加するだけです。
非常にメンテナンスが楽ですね。
たったこれだけで、メールフォームからのSPAMは1通も来なくなりました。
将来的にはJavaScriptを解釈してメールを送信してくるSPAM botも現れるかもしれませんが、個人サイトレベルであれば現状この程度で十分だし、ユーザーにとっても負担がなくて良いのではないかなと思いました。
追記: jQueryを使わない場合
jQueryを使っていないサイトの場合、これだけのためにわざわざjQueryを入れるよりはJavaScriptで書いたほうが軽いのですが、 @zenkaku さんと id:kuwa さんにJavaScript版をはてブのコメントで教えていただきましたので掲載させていただきます。
JavaScript
1 2 3 | <script type="text/javascript"> document.getElementById('formtest').value = '7hs4sk2'; </script> |