クロスサイトスクリプティングを回避する方法を探るの記事に引き続き、Webアプリの脆弱性を見る。

今回見るのはSQLインジェクション(SQLi)にする。


SQLiが何なのか?の説明に入る前にサンプルコードで挙動を確認してみる。




SQLiを試す為のファイルの構成は下記の通り。

.
├── confirm.php
├── db
│   └── sample.db
└── index.php

SQLiはデータベースにアクセスする攻撃なので、SQLiteでデータベースを用意しておく。

テーブルスキーマは下記の通り。

CREATE TABLE user (
	id INTEGER PRIMARY KEY AUTOINCREMENT, 
	login_id VARCHAR, 
	pw VARCHAR
);

データベースは下記のような値を用意しておく。

idlogin_idpw
1usakopassword
2kumatapassword

※pw(パスワード用のカラム)の値が平文だけれども、検証用で平文にしているだけで、実際は必ず一方向ハッシュ関数をかましてから値を格納する必要がある

Goで一方向ハッシュ関数によるパスワードの暗号化を書いてみた


index.php(ログインフォーム)

<form action="confirm.php" method="post">
	<div>
		<label>ID</label>
		<div><input type="text" name="login_id" required></div>
	</div>
	<div>
		<label>パスワード</label>
		<div><input type="password" name="pw" required></div>
	</div>
	<div>
		<input type="submit">
	</div>
</form>

confirm.php(ログイン後の確認画面)

$dbh = new PDO("sqlite:" . dirname(__FILE__) . "/db/sample.db", "", "");

$sql = "SELECT * FROM user WHERE login_id = '" . $_POST["login_id"] . "' AND pw = '" . $_POST["pw"] . "';";
var_dump($sql);
echo "<br>";
$result = $dbh->query($sql)->fetchAll(PDO::FETCH_ASSOC);

if(count($result)){
	foreach($result as $v){
		echo $v["id"] . "<br>";
		echo $v["login_id"] . "<br>";
		echo $v["pw"] . "<br>";
	}
}else{
	echo "データなし";
}

confirm.phpではログインできたら、ログインしたユーザの情報を表示するという内容を設けた。




早速、SQLインジェクションを仕掛けてみる。



IDの項目には悪意のあるコードを、パスワードには明らかに短い文字列を入力して送信してみると、



データベースに格納されているすべてのユーザのデータが表示されてしまった。

実行したSQLを確認してみると、

SELECT * FROM user WHERE login_id = 'dora' OR pw != '';'' AND pw = 'a';

パスワードが空でないすべてのユーザを取得するという文になっていた。


今回利用しているPHPのPDOでは、SQLの一文の以降にある文はエラーにならずに弾かれるらしい。

PDOのqueryで、一文以上の文字列の場合はエラーになるという設定があれば、このコードは弾かれるようになるだろうけど、ここではPDOの設定に関して深堀することはしない。

PHP: PDO - Manual




SQLインジェクションの問題に対して、プリペアードステトメントという手法が有効だとされている。

プリペアドステートメントの詳細を説明する前に、改修内容を記載する。


confirm.php

$dbh = new PDO("sqlite:" . dirname(__FILE__) . "/db/sample.db", "", "");

$sql = "SELECT * FROM user WHERE login_id = :login_id AND pw = :pw";
$stmt = $dbh->prepare($sql);
$stmt->execute(array(":login_id" => $_POST["login_id"], ":pw" => $_POST["pw"]));
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

if(count($result)){
	foreach($result as $v){
		echo $v["id"] . "<br>";
		echo $v["login_id"] . "<br>";
		echo $v["pw"] . "<br>";
	}
}else{
	echo "データなし";
}

index.phpでフォームに入力した値をSQL文にそのまま組み込まず、実行直前に各カラムに対応する値を指定する。

このコードであれば、フォームのIDの項目にdora' OR pw != '';'を入力して送信しても、SQL実行時にidカラムの値としてSQLが実行されるようになる。

PHP: プリペアドステートメントおよびストアドプロシージャ - Manual



先程と同じでidの項目にdora' OR pw != '';'、パスワードには明らかに短い文字列を入力して送信してみると、データなしという文字列が表示された。


この攻撃であれば、アプリを作る時に必ずプリペアドステートメントをかましているので、見落としがない限り、SQLインジェクションの被害はないだろう。

SQLインジェクション - Wikipedia


関連記事

SOY2DAOでプリペアードステートメントを利用する