SMART LLC

プリペアドステートメント(静的プレースホルダ)でSQLインジェクション対策

公開日:2014/10/04

SQLインジェクション攻撃。webの勉強を始めるまで知らなかった。
テキスト入力項目に不正な値を入力して改竄SQLを発行する方法。
本来のSQL文を打ち消した上でまったく別のSQL文を発行したり複数のSQL文を発行したり。
レコードをDELETEしたりテーブルをDROPしたり個人情報や内部情報をSELECTしたり_(:3 」∠)_ 恐ろしい
PHP+MySQLの環境でSQLインジェクションを防ぐ方法をメモしとく。

プリペアドステートメント

SQLインジェクションを防いでセキュアなDBアクセスを実現するのがプリペアドステートメント。
静的プレースホルダとも言う。どっちの呼び方がメジャーなのかはわからない。
'SELECT TEST * FROM WHERE TEST='.$testみたく自前で入力値を結合したSQL文をDBMSに渡すのではなく、DBMS側でSQL文を組み立てる。
まずDBMSに入力値が埋め込まれるSQL文だけを先に渡す。今から実行するSQL文はこの形ですよろしくねみたいな。
形が確定したとこに後から埋め込む入力値を渡す。それをDBMSが組み立てる。
組み立てた結果が意図した形と違う場合は実行しない。
しかも入力値にコーテーションをつけたり数値チェックもやってくれる( ゚Д゚)ウマー
さらに連続で同じ形のSQL文を発行する場合、SQL文は最初の1回だけ渡して2回目以降は値だけのやりとりになる。
とてもスマートである。

これに対して動的プレースホルダてのもある。同じことをDB接続用のAPIで実装する方法。
この場合はSQLインジェクションを100%防ぐことはできないとされている。
まあPHP+MySQLにおいては100%SQLインジェクションを防げる静的プレースホルダがある以上、あえて選択する場面はないでしょう。
手動でSQLを結合するよりは安全だろうけど。
ちなみにPHPマニュアルでは静的プレースホルダのことをサーバーサイドのプリペアドステートメント、動的プレースホルダのことをクライアントサイドのプリペアドステートメントて言ってる。

PHP+MySQLの接続用API

プリペアドステートメントの前にPHP+MySQLの接続用APIについて。
PHP+MySQLの接続用APIはmysql、mysqli、PDOの3つがある。
mysqlは将来削除される予定で現在では非推奨なので却下。
mysqliとPDOはどちらも現在も開発中。パフォーマンスも変わらないぽい。
決定的な違いとしてPDOは他のDBMS用にもAPIがあるのでDBMSの変更があった時にコードをそのまま流用できる。
というわけでPDO採用で決定(σ・∀・)σ
使わないから関係ないけどmysqliは動的プレースホルダを実装していない。

PDOクラスの生成

PDOクラスを生成する関数をつくってみた。

function db_connect() {
	$ini = parse_ini_file('dbconnect.ini');
	$dsn = $ini['dsn'];
	$user = $ini['user'];
	$password = $ini['password'];

	$options = array(
		PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
		PDO::ATTR_EMULATE_PREPARES => false,
		PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
	);

	$pdo = new PDO($dsn, $user, $password,$options);
	return $pdo;
}

PDOを生成するパラメータはDSN、ユーザ名、パスワード、あとオプション。
DSN、ユーザ名、パスワードは変更の可能性とセキュリティを考えてiniファイルから取得。
iniファイルは非公開ディレクトリに置くこと。
オプションは3つ指定。配列化してパラメータにセットする。
まずERRMODEをPDO::ERRMODE_EXCEPTIONにすることで例外を返すようになる。デフォルトで例外が返らないという( ゚д゚)
2つ目でEMULATE_PREPARESをfalseにしてるのが静的プレースホルダを使う設定。trueが動的プレースホルダ。プリペアドステートメントをAPI側でエミュレートて意味か。
最後にSET NAMES utf8で文字コードを指定。INIT_COMMAND自体はDBMS接続時に毎回実行するコマンドを設定する項目。
あとは各パラメータを元に生成したPDOオブジェクトを関数の呼出元に返す。

dbconnect.iniの中身はこんな感じ。

dsn='mysql:dbname=test;host=localhost'
user='test'
password='password'

文字コードで結構ハマッた気がするけどちょっと前のことだから覚えてない。
やっぱりメモは重要。

INSERT文の発行

INSERT文を発行する関数をつくってみた。

function db_insert($sql,$rows) {
	try{
		$pdo = db_connect();
		$pdo -> beginTransaction();
		try{
			$stm = $pdo -> prepare($sql);
			foreach ($rows as $cols) {
				$stm -> execute($cols);
			}
			$pdo -> commit();
			return true;
		} catch (PDOException $e){
			$pdo -> rollback();
			throw $e;
		}
	}  catch (PDOException $e){
		return false;
	}
}

関数のパラメータは2つ。入力値が埋め込まれるSQL文と埋め込む入力値。
入力値は配列の配列を受け取る。カラム配列をレコードに配列に格納して渡すことになる。
まず先に作成した関数を呼び出してPDOクラスを生成。
PDOクラスができたらbeginTransactionメソッドでトランザクションを開始。
次に1度だけprepareメソッドを使ってSQL文をDBMSに渡す。
あとはforeachでループして行数分だけexcuteメソッドを実行。パラメータで入力値の配列を渡す。DBMSに渡して組み立てたものが発行される。
SQLインジェクション攻撃を受けた場合、ここで例外が発生する。
全行ループし終わったらcommitメソッドでトランザクションをコミットしてtrueを返す。
try catchを使ってトランザクション開始後に例外が発生した場合はrollbackメソッドでトランザクションをロールバックすると同時にひとつ外のtry catchにthrow。
外のtry catchで例外が発生した場合はfalseを返す。
この例外処理もけっこうハマッたけど少し前のことなのでどうハマッたか覚えてない(´・ω・`)ショボーン
まあ結果これでうまく動作するようになってるからOK。

呼出元。

$col1 = $_POST['col1'];
$col2 = $_POST['col2'];
$col3 = $_POST['col3'];
$col4 = $_POST['col4'];

$sql = 'INSERT INTO `TEST` VALUES (:col1,:col2,:col3,:col4)';
$cols = array($col1,$col2,$col3,$col4);
$rows = array($cols);

try {
	if (!db_insert($sql, $rows)){
		throw new Exception('db_insert failed');
	}
	
	echo '成功';

} catch (Exception $e) {
	echo '失敗';
}

これは1行だけINSERTする場合。
POSTされた各カラム($col1~$col4)を格納したカラム配列($cols)をレコード配列($rows)に格納。
SQL文($sql)と一緒に上記関数に渡す。
falseが返ってきたら例外を発生させて「失敗」と表示する。

SHARE