Hatena::ブログ(Diary)

甘味志向@はてな RSSフィード



10年02月19日

PHPで誰でも簡単Webサービス製作!でなんか作って公開した奴ちょっと来い

タイトルは出来れば関連する方に読んで欲しかったので、軽く釣り針にしました。すみません。:*)


最近はやりのヒウィッヒヒーTwitter)でも、よく「○○ったー」みたいなサービスがばんばん登場してますね!

おかげでますますツイッターが面白い感じになってて、いい流れですね!


でも・・・ちょっと気になることが・・・



最近「もうプログラマには頼らない!簡単プログラミング!」だとか・・・ 「PHPで誰でも簡単Webサービス作成!」だとか・・・

はてなブックマークホッテントリで見かけますよね・・・


プログラミングする人が増えるのは素敵です!レッツ・プログラミングなう! なんですけど・・・



ちゃんとセキュリティのこと考えてますか・・・!?


『セキュリティ対策とか難しいし面倒くせーし、俺の適当に作ったサービスとかどうなってもイイしww』


いいんですいいんです! 別にそう思ってるならどうでもいいんです!




でもそんなプログラムをWeb上で公開すんじゃねーよボケと。




作ったあなたはどうでも良くても、知らずに使ったユーザーさんが事件に巻き込まれるんですよ。

安易に作った、ただ動くだけのプログラムをユーザーさんは信用して利用するわけです。


そこを少しでいいから考えて欲しい。





1.XSSっていうのがこわい!


まずこちら、どこにでもあってはいけないフォームです。

	<form action="/test.php" method="get">
		IDをいれてね☆ : <input type="text" name="id" size="30">
		<input type="submit" value="送信☆">
	</form>

	<?php
		// フォームで名前が送信されてたら、名前を表示!
		if($_GET['id']!=NULL) {
		echo "<p>ようこそ! ".$_GET['id']."さん!!</p>";
		}
	?>

f:id:Molokheiya:20100219161447p:image


「Molokheiya」と入力して送信すると、「ようこそMolokheiyaさん」と出るだけの簡単なプログラムですね。Hello World


でもここに、「<script>alert(1);</script>」と入力して送信したら・・・?


f:id:Molokheiya:20100219161448p:image


ななな、なんか出た!?

フォームに入力したHTMLとかJavaScriptが、そのまま実行できちゃう・・・!?


・・・ってことは、悪質なウイルスを仕込んだページに飛ばしたり、えっちぃサイトをフレームで埋め込んだり、グロテスクな画像を表示させるとかが簡単に出来ますね!

もう少し工夫すれば適当なサイトをフレームで表示させて、OKボタンだけを見えるように位置調節、そしてOKをユーザーに気づかれないまま押させる・・・とかも出来るかな?


そんなことないって言うけど、ついったーでみんなやってる! って先入観がある状態で「OKを押して下さい」って出たら、みんなやってる安心感で押しちゃってもおかしくないと思うな!



さらに恐ろしいのは、ログイン機能とかがあると、ログイン状態の乗っ取りが出来るってことかな・・・



こちら。これもセキュリティ的に存在してはいけないプログラム。

	<form action="/test.php" method="get">
		IDをいれてね☆ : <input type="text" name="id" size="30">
		<input type="submit" value="送信☆">
	</form>
	
	<?php
	session_start(); // セッションを使うよ
	
		// フォームで名前が送信されてたら、セッションに登録!
		if($_GET['id']!=NULL) {
			$_SESSION['name']=$_GET['id'];
		}
		
		// セッション「name」にデータがあったら、それを表示!
		if($_SESSION['name']!=NULL) {
			echo "<p>ようこそ! ".$_SESSION['name']."さん!!</p>";
		}

	// 変数の中身を書き出してみる
	echo "GET['id'] : {$_GET['id']}";
	echo "<br>";
	echo "SESSION['name'] : {$_SESSION['name']}";
	?>

PHPの便利機能、セッション。あちこちの有名サイトでも、ログイン管理に使われてますね!

でも使い方を誤ると大変なことに・・・!



最初ページを表示すると、GETもSESSIONもデータがないのでカラッポです。

f:id:Molokheiya:20100219161450p:image


さっきと同じようにIDを入れて送信します。

すると『$_SESSION['name']=$_GET['id'];』で、IDがセッションの中に記録されますね。

f:id:Molokheiya:20100219161449p:image


一度セッションに記録されると、セッションが切れるまでずっとデータが残る!

しかもセッション側のデータはサーバーに保存されるので、システムで使うパラメータとかも比較的安全に保存しておけるのです。

だから、ログイン状態などはセッションで管理されるんですね。

f:id:Molokheiya:20100219161451p:image



このセッションは、サーバー側のデータとブラウザを繋ぐのに「セッションID」をクッキーで保存しています。

f:id:Molokheiya:20100219165330p:image

初期状態だと、「PHPSESSID」という名前になります。この中の英数字が、サーバー側のデータを引き出すキーです。


・・・もうピンと来ちゃいましたか?


要はセッションIDをゲット出来れば、パスワードやIDを知らなくてもログイン状態を引き継ぐ(乗っ取る)ことが出来るんですね!


これをセッションハイジャックと言います。


さて、さっきは「alert(1);」を入れましたが、ここに「alert(document.cookie);」を入れるとどうなるのでしょう!!


f:id:Molokheiya:20100219165331p:image



本当に恐ろしいですね。



対策

変数を書き出す時は、必ずエスケープを行いましょう。


非常に簡単な対処で、ほとんどの攻撃を阻止できるようになります。

	<form action="/test.php" method="get">
		IDをいれてね☆ : <input type="text" name="id" size="30">
		<input type="submit" value="送信☆">
	</form>
	
	<?php
	// フォームで名前が送信されてたら、名前を表示!
	if($_GET['id']!=NULL) {
	echo "<p>ようこそ! ".h($_GET['id'])."さん!!</p>";
	}

	function h($str) { // XSS対処
		return htmlspecialchars($str,ENT_QUOTES,'UTF-8');
	}

全ての変数のHTML書き出しには「htmlspecialchars」を通します。


2/20 11:50 追記

<form action="<?=$_SERVER['PHP_SELF']?>" method="get">

実はここにもXSSの脆弱性が含まれています。次のようなURLでアクセスされた場合、任意のスクリプトを実行することが出来ます。

http://www.example.jp/ 
example.php/%22%3E%3Cscript%3Ealert(document.cookie);%3C/script%3E

これはネタ元で対策方法として「変数を書き出す時は、必ずエスケープを行いましょう。」と書きながら$_SERVER['PHP_SELF']をエスケープしていないのが原因でしょう。

(注意:最近ではこのようなスクリプトはブラウザがブロックしてくれるのですが、IE6とか古いブラウザもありますしね)


Re:PHPで誰でも簡単Webサービス製作!でなんか作って公開した奴ちょっと来い - to-R


とのことです。ご指摘ありがとうございました!!

なお現在は対策したコードにしてあります。すみません!


f:id:Molokheiya:20100219165332p:image

f:id:Molokheiya:20100219165333p:image


htmlspecialchars: 特殊文字を HTML エンティティに変換する (String 関数) - PHPプロ!マニュアル


第3引数文字コードは適切変更して下さい。


ここでは長くなるので省略しますが、セッションハイジャックへの対策は session_regenerate_id(true); 等があります。

あと次回からログインを省略したいからといって、クッキーの保存期間を延ばしてはいけません。

自動ログインは、自動ログイン用のクッキーを発行して行いましょう。

クッキーに保存する情報は出来るだけランダムな値になるようにします。

ユーザー名やパスワードをハッシュにしたものを保存して認証するのは、あまり良いとは言えません。

詳細はこちらを読んでください。




2.SQLインジェクションっていうのがこわい!


皆さんSQL使ってますか!


あらゆるデータを効率的に管理してくれるデータベースは、すこぶる素敵ですよね。

私はMySQLとか好きです!


ということで、ユーザー名とパスワードを保存するデータベースを用意してみましょう!


まず適当にテーブルを作ってみました。

mysql> desc user;

+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int(11)     | NO   | PRI | NULL    | auto_increment |
| userid   | varchar(30) | NO   | UNI |         |                |
| password | varchar(50) | NO   |     |         |                |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

「id」が連番、「userid」が30文字までのユーザーID、「password」がパスワードという単純構造です!

そしてテストデータをポイっと入れます。


mysql> SELECT * FROM user;

+----+------------+----------+
| id | userid     | password |
+----+------------+----------+
|  1 | hoge       | hagehoge |
|  2 | foo        | gaybarrr |
|  3 | Molokheiya | 1231233  |
|  4 | bar        | hugahugo |
+----+------------+----------+
4 rows in set (0.00 sec)

ちなみにこの時点で既に論外です。



ユーザー名とパスワードでログイン認証をするとしましょう!

そしたら、PHPでこういうのが出来るかと思います。


これも存在するはずのないプログラムですよ。

	<form action="/test.php" method="post">
		IDをいれてね☆ : <input type="text" name="id" size="20"><br>
		PWをいれてね☆ : <input type="password" name="pass" size="10"><br>
		<input type="submit" value="送信☆">
	</form>
	
	<?php
	if($_POST['id']!=NULL) {
		if($db=mysql_connect('localhost','molo_sql','sqlpassword') or die("接続しっぱい!")) {
			mysql_select_db('molo_sql',$db); // データベースを選択
			mysql_query("SET NAMES utf8"); // 文字コードをUTF-8にして、日本語が文字化けしないように
		}
	
	$name=$_POST['id'];
	$pass=$_POST['pass'];
		
	$sql="SELECT * FROM user WHERE userid = '{$name}' AND password = '{$pass}';";
	$result=mysql_query($sql); // SQLを実行
		
		// 行数がゼロならパスワードが違う? から認証失敗
		if(mysql_num_rows($result)==0) {
		echo "ユーザー名かパスワードが違うよ><";
		} else {
		// 認証成功?
		$row=mysql_fetch_assoc($result);
		echo "<span style=\"color:red;\">ようこそ!".h($row['userid'])."さん!</span>";
		}
	}

	function h($str) { // XSS対処
		return htmlspecialchars($str,ENT_QUOTES,'UTF-8');
	}
	?>

これでユーザーIDに「Molokheiya」、パスワードに「1231233」と入力して実行!


成功すると、こうなります! やったね!

f:id:Molokheiya:20100219165334p:image


この時のSQLは

mysql> SELECT * FROM user WHERE userid = 'Molokheiya' AND password = '1231233';

+----+------------+----------+
| id | userid     | password |
+----+------------+----------+
|  3 | Molokheiya | 1231233  |
+----+------------+----------+
1 row in set (0.00 sec)

で、バッチリ取れていることが分かります!


・・・さて、ではユーザーIDに「0」、パスワードに「' OR '1' = '1」と入力して実行すると、一体どうなるのでしょう。


f:id:Molokheiya:20100219165335p:image


hogeさんとしてログイン出来ました。


この時のSQLは

mysql> SELECT * FROM user WHERE userid = '0' AND password = '' OR '1' = '1';

+----+------------+----------+
| id | userid     | password |
+----+------------+----------+
|  1 | hoge       | hagehoge |
|  2 | foo        | gaybarrr |
|  3 | Molokheiya | 1231233  |
|  4 | bar        | hugahugo |
+----+------------+----------+
4 rows in set (0.00 sec)

SQL文を書き換えて、どんなID・パスワードでも全ての行が取得出来るようになってしまいました・・・!!


このように意図しないSQLを注入(injection)することから、この攻撃はSQLインジェクションと呼ばれます。



この攻撃は、何もユーザーログイン時だけに限りませんよ?

非表示にしておきたい論理削除のデータを引っ張りだすことから、全顧客情報を流出させることまで、ブラウザだけで簡単にできます。怖い。





対策

SQLに組み込むパラメータは必ずエスケープしましょう。


こちらも簡単にほとんどの攻撃を阻止できるようになります。


まずこの例ですが、パスワードを平文でデータベースに保存している時点で既に論外です。


パスワードは必ずSHA1でハッシュにして、データベースに保存します。

MD5でも構いませんが、MD5よりもより強力なSHA1を出来るだけ利用しましょう。


参考

市販のメールソフトを用いた調査の結果、想定した全てのパスワードについて、

市販されているPCを用いて、比較的短時間で解読できることが確認できました

「MD5 の安全性の限界に関する調査研究」に関する報告書:IPA 独立行政法人 情報処理推進機構


PHPですとsha1という関数がありますから、簡単にハッシュを利用できます。

sha1: 文字列の sha1 ハッシュを計算する (String 関数) - PHPプロ!マニュアル

またMySQLの場合、SQLでSHA1にすることも出来ます。



mysql> SELECT * FROM user;

+----+------------+------------------------------------------+
| id | userid     | password                                 |
+----+------------+------------------------------------------+
|  1 | hoge       | cc624022d67e29993916cf116f8696d2aebb6e9f |
|  2 | foo        | 9534d1983d3235a39fd4f1d4f8e5ece7984e0071 |
|  3 | Molokheiya | a971f78af101664553ac2e6d2997b1f7b491936f |
|  4 | bar        | 044e596b4d04f6733bb03c6a4283c740d21f1350 |
+----+------------+------------------------------------------+
4 rows in set (0.00 sec)


そしてSQLに組み込むパラメータのエスケープには、mysql_real_escape_stringを使います。

mysql_real_escape_string: SQL 文中で用いる文字列の特殊文字をエスケープする (MySQL 関数) - PHPプロ!マニュアル


さっきのhtmlspecialcharsと同じように名前が長いので、1文字の関数にしておきましょう。

<?php
	function x($str) { // SQLインジェクション対処
		return mysql_real_escape_string($str);
	}

するとユーザーIDに「0」、パスワードに「' OR '1' = '1」を入力したSQLはこうなり、攻撃は成立しなくなります。


mysql> SELECT * FROM user WHERE userid = '0' AND password = '\' OR \'1\' = \'1' LIMIT 1;

Empty set (0.00 sec)

ちなみに結果が必ず1行のみになると分かっている場合は、SQLの最後に「LIMIT 1」をつけます。

これにより、なんらかの事態があっても1行以上が表示されることはありません。

※これはセキュリティ対策というより豆知識です!


さて、このままではSHA1になっていませんからPHPはこうします。

<?php
	$name=x($_POST['id']);
	$pass=sha1(x($_POST['pass']));
		
	$sql="SELECT * FROM user WHERE userid = '{$name}' AND password = '{$pass}' LIMIT 1;";

まさに、数文字付け足すだけですね。





とりあえず、素人が最もやってしまいがちな重大脆弱性を2つ例にとってみました。


HTMLへの変数書き出しは、必ずh関数を通す!

<?php
	function h($str) { // XSS対処
		return htmlspecialchars($str,ENT_QUOTES,'UTF-8');
	}

SQLへの変数組み込みは、必ずx関数を通す!

<?php
	function x($str) { // SQLインジェクション対処
		return mysql_real_escape_string($str);
	}

これだけで良くある脆弱性のかなりを塞げるはずです。

もちろんこれだけでは不十分で、PHPは誰でも簡単にサービスを作れるのと同時に、たくさんの脆弱性を作れてしまいます。


このページは上記した脆弱性や、それ以外の脆弱性についても詳しく対策がありますので、少しでも興味を持った方は是非見てみて下さい。

PHP と Web アプリケーションのセキュリティについてのメモ


ただ全てに共通するセキュリティ対策の鉄則として、「与えられたデータを信用するな」というのがあります。


電話番号を入力するフォームで、送られてくるのが半角数字になるとは限りません。きちんとそれが半角数字かどうか、is_numeric等を使ってチェックしてください。

TwitterIDを入力するフォームで、HTMLタグや悪意を持ったスクリプトが送信されないとは限りません。正規表現等を使ってチェックしてください。



とりあえず動けばハイ完成。ではなく、利用するユーザーさんの安全まで考えてWebサービスを公開して下さい。

それが出来ないというなら利用するのは自分や身内だけにとどめて、不特定多数に向けては公開しないようにしましょう。





あなたのプログラムが怒りや悲しみではなく、笑顔をもたらしますように。

wen000wen000 2010/02/20 14:43 http://mangaconnect.sp.land.to/
前にこういうのを作ったのですが、ログインとかがないので個人情報は扱ってないとはいえ、セキュリティについてはどれだけやっても無駄にはならないよなあと痛感しました。

pochi-ppochi-p 2010/02/20 15:20 こんにちは。
その心意気は良いと思います。
でもブコメでまだツッコミあるから見といた方がいいですよ〜。頑張ってください。

私もプリペアードステートメントの方がオススメですね。>SQLインジェクション対策

個人的な趣味で言うと、コリジョン対策兼40文字オーバー可にする為、MD5+SHA1を繋げちゃう等した方がいいかなと思いました。
まあ一般的に使われてる定石がどんなのかは知らないのですけども。

fool4rootfool4root 2010/02/21 03:13 is_numeric()は半角数字かどうかの判定には使えませんよ。
16進数や+記号、小数点もtrueです。

SixbyteSixbyte 2010/02/28 01:07 実際XSSを悪用する場合には、PersistentなXSSでない限り他のサイトを経由させないと悪用できないし、IDの収集が目的であれば、攻撃者自身が簡単な「○○ったー」を作ってID等の情報を入力させた方が遥かに楽です。そのサービスが情報収集を目的とした攻撃者によるサービスである危険性を考慮した上で利用することも重要ですね。

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。