生まれてはじめて自分でスクリプト(簡単なリンク集)を作成してみました。
http://hatena88.web.fc2.com/hatena/newpage1.html
これについて、下記の点を留意しつつ、
もっとスマートにするにはどうしたら良いか、
いろいろ細かく、やさしくつっこみを入れてください。
・削除ボタンを押すと、その行全体が削除するという風にしたいのですが
うまく削除できません。これを解決してほしいです。
・テーブル名も変数にしたいが、エラーが出てうまくいかなかったです。
・セキュリティ対策はこれでよいのか?
・フォームから送信される値を全部配列として処理したい。
つまり項目が増えてもいちいちそれを書き直さなくても良いようにしたい。
その他どんな細かいことでも教えていただけると嬉しいです。
私ならこうするといった代替案でも結構です。
なお回答には理論だけでなく、
実際に動くスクリプトをお示しくださいますようお願いします。
>うまく削除できません。
削除ボタンを押した場合、hiddenでの名前が"del"になっているためです。ので"del"を"id"にすればokでしょう。ただ<form>が閉じていないので正しいidが戻っていません。</form>で閉じることで正しいidは戻りますが$_POST["title"]が設定されていないと
if (!isset($_POST["title"])){
の部分が実行されて終了していました。
>$query = "insert into site (title,category,launguage,link_ok,url,server_name,uptime)";
>この部分の項目が増減してもいちいち書き直さないでも良いようにしたいと言うことです。
php側だけではなくMySQL側でも直さなくてはならないのでその都度(頻繁に変更があることの方が問題)、書き換えでもいいと思います。
あるいは新たに追加がある場合はその部分だけのupdate文で処理するようにするなど。
エラー処理はやっかいです。ユーザーには詳細な内容を知らせない方がいいですし、自分には詳細な内容が必要です。ある程度の記述が必要ですのでset_error_handler()が便利です。後はエラーが発生したらtrigger_error()を呼ぶだけでset_error_handler()で指定した関数へ飛びます。
phpはhtml文の中に混在して書き込めるのが特徴の一つです。単純な場合はそれでいいのですが、複雑になるとこれが逆に足かせになってきます。今回は更にSQL文もあるので三つのものが混在しています。
提案としてはこれらはなるべく別々にするようにし、それらをつなぐものとして配列を使います。
$_POST,$_GET,$_COOKIE,$_SERVERなどのPHPの外部から来る変数は信用できません。
ので使用には常に注意が必要です。常に言えることは直接使ってはダメということです。
SQLインジェクションについて記憶に新しいものとしてはトレンドマイクロがあります。
http://jp.trendmicro.com/jp/about/notice/0312/index.html
考え方として攻撃を無害化するというのがmysql_real_escape_stringに代表されるものでしょう。
私の場合は攻撃を感知するということに重点を置いています。どういう攻撃があるかがわからないと感知できないのが難点ですが。
考え方が違えば、やり方も変わってきますし、どれが正解ということはないと思います。ようは攻撃を防げればいいのですから。
prepared文はMySQL 4.1から追加されたようです
http://lists.mysql.com/mysql-ja/77
これを使えば、あらかじめquery文を評価してしまい、後でexecute文で引数で置き換えて実行するというものです。
ので外部から与えられるものは単なるデータになるのでSQLインジェクションにはならないと理解しています(多分)。
プリペアドステートメントに渡すパラメータは、引用符で括る必要は ありません。それはドライバが自動的に行います。 アプリケーションで明示的にプリペアドステートメントを使用するように すれば、SQL インジェクションは決して発生しません (しかし、もし信頼できない入力をもとにクエリの他の部分を構築している のならば、その部分に対するリスクを負うことになります)。
mysql_xxxでは使えないようなのですがPEAR::DBなら使えます(多少オーバーヘッドは発生するでしょうが)。
php5ではPEAR::DBがうまく動かなかったのでPDOで実装しました。mysqliが改良版拡張サポートということでprepared文が使えるのですが使いかたが面倒そうなので断念しました。
<?php define('CR', "\n"); define('BR', "<br>"); define('DB_NAME', 'link'); define('USER', 'xxxxxx'); define('PASSWD', 'xxxxxxx'); //date_default_timezone_set('Asia/Tokyo'); function error_handler($errno, $errstr, $file, $line, $context) { static $errortype = array ( E_ERROR => 'Error', E_WARNING => 'Warning', E_PARSE => 'Parsing Error', E_NOTICE => 'Notice', E_CORE_ERROR => 'Core Error', E_CORE_WARNING => 'Core Warning', E_COMPILE_ERROR => 'Compile Error', E_COMPILE_WARNING => 'Compile Warning', E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', E_STRICT => 'Runtime Notice', E_RECOVERABLE_ERROR => 'Catchable Fatal Error'); echo $errortype["$errno"].':'.$file.':'.$line.'<br>'.$errstr.'<br>'.'<br>'; return TRUE; } function mysql_prepared($sql, $parms=NULL) { $mysql = new PDO('mysql:host=localhost;dbname='.DB_NAME, USER, PASSWD); $stmt = $mysql->prepare($sql); $stmt->execute($parms); return $stmt->fetchAll(PDO::FETCH_ASSOC); } function mysql_insert($parms) { $sql = "insert into site (title, category, launguage, link_ok, url, server_name, uptime)". " values (?, ?, ?, ?, ?, ?, CURRENT_DATE())"; return mysql_prepared($sql, $parms); } function mysql_delete($parms) { $sql = "DELETE FROM site WHERE id = ?"; return mysql_prepared($sql, $parms); } function table_tr_html($attr, $tr_ary) { $contents = ''; foreach ($tr_ary as $col) $contents .= '<td>'.$col.'</td>'; return '<tr'.$attr.'>'.$contents.'</tr>'; } function table_html($table_ary) { $contents = table_tr_html(' bgcolor="#88bbff" height="32"', array_shift($table_ary)); foreach ($table_ary as $id=>$row) $contents .= table_tr_html($id%2 ? ' bgcolor="#fafaff"':'', $row); return '<table>'.$contents.'</table>'; } function db_table_html($db_data) { $map = array(); foreach ($db_data as $row) $map[] = array( $row['id'], BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'. '<input type="submit" name="op" value="削除">'. '<input type="hidden" name="id" value="'.$row['id'].'">'.'</form>', $row['title'], $row['category'], $row['launguage'], $row['link_ok'], str_replace(array('http://','www.'),array('',''),$row['url']), $row['server_name'], date('Ymd',strtotime($row['uptime']))); return $map; } function validate($vars, $request) { $parms = array(); foreach ($request as $name) if (isset($vars["$name"])) $parms["$name"] = htmlspecialchars($vars["$name"]); return $parms; } set_error_handler("error_handler"); $request = array('id','','title','category','launguage','link_ok','url','server_name'); $parms = validate($_POST, $request); if ($parms && isset($_POST['op'])) { if ($_POST['op'] === '新規') mysql_insert(array_values($parms)); else if ($_POST['op'] === '削除') mysql_delete(array($parms['id'])); } $db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC"); $db_table = array_merge(array(array_merge($request, array('uptime'))), db_table_html($db_all_data), array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'. '<input type="submit" name="op" value="新規">', '<input type="text" name="title" value="タイトル">', '<input type="text" name="category" value="カテゴリー">', '<input type="text" name="launguage" value="言語">', '<input type="text" name="link_ok" value="リンク">', '<input type="text" name="url" value="url">', '<input type="text" name="server_name" value="サーバー">'.'</form>',''))); restore_error_handler(); ?> <html> <head> </head> <body> <?= table_html($db_table) ?> </body> </html>
動作確認はphp5、mysql4.1以上でdefineのUSER,PASSWDを設定してあれば動くとは思います。
error_handler()
set_error_handler("error_handler")としているのでエラーがあればここに飛んできます。そのまま表示していますのでユーザーには別のメッセージを表示して、自分にはメールを飛ばすようにするなどにした方がいいでしょう。
mysql_prepared()
prepared文を使ってquery()します。
mysql_insert($parms)
mysql_prepared()使って挿入します。挿入する内容についてはチェックしていません。prepared文を使っているのでSQLインジェクションの問題やhtmlspecialchars()が施されているので大まかな問題は解決されていると思いますが、あまりにも長すぎるとか不適切な内容などについては調べる必要があるでしょう。
mysql_delete($parms)
mysql_prepared()使って削除します。idについてはチェックしていませんが不適切なidなら削除されないだけなので実害は無いという判断です。
table_tr_html($attr, $tr_ary)
htmlの<tr>..</tr>の部分を生成します。
table_html($table_ary)
htmlの<table>..</table>の部分を生成します。
db_table_html($db_data)
dbの内容(二次元)をhtmlに変換する前段階として各要素を多少変更します。また削除のボタンを追加しています。
validate($vars, $request)
セキュリティの要としています。この部分をパスするということは処理を続行しても問題無い、あるいは大きな問題は無いと判断したということにしています。今回はhtmlspecialchars()だけしていますが、$requestで受け取るべき名前を列挙しているので、足りなかったり、逆に$requestにないものが$_POSTにあったら処理を止めるなどが可能です。
以下がメインの処理です。
set_error_handler("error_handler");
"error_handler"を設定します。エラーや問題を検知した場合はtrigger_error("メッセージ", E_USER_ERROR)で通知するようにしています。
$request = array('id','','title','category','launguage','link_ok','url','server_name');
$_POSTで受け取る名前を列挙します(テーブルのヘッダーとしても利用しているので途中に空の要素を入れていますが)。
$parms = validate($_POST, $request);
$_POSTの中で$requestで列挙されたものだけhtmlspecialchars()して$parmsに取り出します。
if ($parms && isset($_POST['op'])) {
if ($_POST['op'] === '新規')
mysql_insert(array_values($parms));
else if ($_POST['op'] === '削除')
mysql_delete(array($parms['id']));
}
ボタン用に"op"という名前を追加しています。本来"op"も$requestの中に入れて$parms['op']とすべきですが多少処理が長くなるので$_POSTを直接使っています。単純に新規(追加)、削除をしていますが追加の場合は、重複しないかチェックするなどの処理が必要かもしれません。新規の場合、prepared文の?と$parmsの数が合ってないとエラーになってしまいます。<input>でテキストが空の場合、送られないと思うので、それがvalidate()で処理されず、数が合わなくなってしまいます(既知のバグ)。
$db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC");
データを全て取り込む。
$db_table = array_merge(array(array_merge($request, array('uptime'))),
db_table_html($db_all_data),
array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.
'<input type="submit" name="op" value="新規">',
'<input type="text" name="title" value="タイトル">',
'<input type="text" name="category" value="カテゴリー">',
'<input type="text" name="launguage" value="言語">',
'<input type="text" name="link_ok" value="リンク">',
'<input type="text" name="url" value="url">',
'<input type="text" name="server_name" value="サーバー">'.'</form>','')));
テーブル用にヘッダー用($requestに'uptime'を追加)、dbからのデータ、新規用の<input>をマージして置きます。
最終的にでhtmlに展開されます。
restore_error_handler();
エラーハンドラーを元に戻します。
スクリプトはキリがないのでいくつかヒントを。(まともに書いたらこの何倍もの量になります。)
フォームから送信される値を配列にする処理は、既に書かれているように見えますが、個別に処理したいなら、
extract($_POST);
で変数に展開するのではなく、
//クエリ生成 // 実際には、クエリを生成する前に$_POSTの値が想定しているものか必ずチェックしましょう。 // つまり、質問のように楽をしようとすることは、現時点では難しいでしょうし、おすすめしません。 $query = 'value('; foreach($_POST as $key => $value) { // ここに処理 $query .= '"' . $value . '", '; } $query .= 'CURRENT_DATE()'; $query .= ')'; // この後、実際にクエリを送る前に、最低限、mysql_escape_string()でエスケープします。
のようにループで回してやるとよいでしょう。
あとはあまりに危険すぎるので、いくつか。
データベースにクエリを送る場合は、実際に送る全てのクエリに対して、最低限、
http://jp2.php.net/manual/ja/function.mysql-escape-string.php
を使ってエスケープしましょう。データが簡単に削除されたりしてしまいますよ。
それから、echoなどで出力する場合は、それも全てhtmlspecialchars($変数, ENT_QUOTES, 'utf-8')などでエスケープしましょう。
$_POSTの値をそのままチェックなし・エスケープなしで表示したり、データベースに送るのは危険です。
今のうちから、変数を出力したり、データベースにクエリとして使う場合は、きちんとその値が想定した範囲のものかチェックしたり、エスケープしておくようにすると後々楽です。
その他、例えば$idはおそらく数字ですよね。数字以外が入ってきた場合はデータベースにアクセスしないようにするなどです。
ご回答ありがとうございます。
>質問のように楽をしようとすることは、現時点では難しいでしょうし、おすすめしません。
そういっていただけるときっぱりあきらめがつきます。
セキュリティは思ってたよりうまくいってませんでしたね。
それがわかってよかったです。教えていただいたことを参考に作り直してみます。
>スクリプトはキリがないのでいくつかヒントを。(まともに書いたらこの何倍もの量になります。)
やっぱりいろいろあるんですね。
もちろん特別気になるところだけでも結構です。大変参考になりました。
なお回答には理論だけでなく、
実際に動くスクリプトをお示しくださいますようお願いします。
プログラムの仕様(目的)やDB構造が明らかではありませんから、そのお願いは無茶というものです。
・セキュリティ対策はこれでよいのか?
すぐに気がついた部分を指摘します。
$sv = "localhost"; $id = "xxxxxx"; $pass = "xxxxxxx"; $db = "link";
これらはセキュアな情報ですので、切り離して、リモートからアクセス不可能なパスに配置すべきです。include関数で読み込んでください。
$value = htmlspecialchars($value);
これだけではSQLインジェクション攻撃を受けます。SQL文に投入する変数については、mysql_real_escape_string 関数でエスケープすべきです。ただし、各々の変数に日本語が入るのかどうか分からないですし、その場合の文字コードが何になるのかも分からないので、一概に mysql_real_escape_string 関数を使えばいいというものではありません。
このようにプログラムの仕様が分からない状況では、具体的なコードをお示しできません。
$sql = "DELETE FROM site WHERE id = {$_POST['id']}";
「SQLインジェクション」攻撃を受けます。リンク先のアドバイスに従って対策を立ててください。
echo "<form method=\"POST\" action=\"".$_SERVER[PHP_MYSELF]."\">";
$_SERVER[PHP_MYSELF] は公式マニュアルに見あたりませんが、$_SERVER['PHP_SELF'] と同等でしょうか? だとしたら、$_SERVER['SCRIPT_NAME'] に置き換えてください。理由は、「PHP PHP_SELFとSCRIPT_NAMEを比較してみた」を参照ください。
・フォームから送信される値を全部配列として処理したい。
コードを見ると、すべて$_POST配列に入っているので、お望みの処理ができるのではないかと思うのですが‥‥。
いつもご回答ありがとうございます。
>プログラムの仕様(目的)やDB構造が明らかではありませんから、
>そのお願いは無茶というものです。
>プログラムの仕様が分からない状況では、具体的なコードをお示しできません。
もっともなことです。私の見識不足で失礼しました。
全部作り直していただかなくても、
一部だけでもわかる範囲でお示しいただければと思いました。
どのような情報を提示すべきか教えていただければお答えします。
それさえもわからぬ初心者ですのでなにとぞ平にご容赦くださいませ。
>リモートからアクセス不可能なパスに配置すべきです。include関数で読み込んでください。
実は気になってた部分だったので、非常に参考になりました。
他のセキュリティも教えていただいたページを参考にしてもっと勉強してみます。
>すべて$_POST配列に入っているので、お望みの処理ができるのではないかと思うのですが‥‥。
これは明らかに質問の仕方が間違えてますね。
データベースにデータを追加以降の項目で、たとえば下記のようになってますが、
$query = "insert into site (title,category,launguage,link_ok,url,server_name,uptime)";
この部分の項目が増減してもいちいち書き直さないでも良いようにしたいと言うことです。
実は削除の項目に取り組んで3日目になるのですが、
どうしてもうまくいかず途方に暮れていました。
不足した情報が何かもお知らせいただければお知らせしますので
今後ご回答いただく皆さんにはこの部分に対するご指導もお待ちしています。
横長に書くと解答欄からはみ出してしまうので、縦長になるように書いています。
一応動作確認はしてみましたが、環境により多少修正が必要かもしれません。
(例えば、内部文字エンコードとか、mbstring.encoding_translation とか、マジッククォートとか)
当方のテスト環境では MySQL が使えないので、SQLite でテストしています。
MySQL を使われるのでしょうから、適宜読み替えて下さい。
関連して、テストフォルダはスクリプトから書き込みできなかったので、直下に
db という書き込み可能なフォルダを作ってテストしました。
DB構造が分からないので、DBのオープン時に適当な値で2レコード追加するようにしています。既にDBが存在するなら、不要です。
削除できないのは、$_POST['id'] から削除するIDを得ているが、削除用IDは $_POST['del'] だからでは?
> フォームから送信される値を全部配列として処理したい。
下記の例で、フィールド名を name="new[title]" にして対応しています。最近はてなで教えてもらった方法です。
これによって、
> つまり項目が増えてもいちいちそれを書き直さなくても良いように
は、部分的には達成できます(下記の例の insertData() や array_walk() )が、
DB定義時のカラム順と表示の順序が一致しないと、このような配列でまわす方法は使えません。
(連想配列は順序を期待しない使い方が本来ですし---PHPでは順序は保持されるらしいけど)
更に、入力値のチェックに個別に最適なものを用いるのが面倒な事になりませんか?
今回は、ゼロ値の削除と、DB用のエスケープ、削除IDが数値である事 をしましたが
URLのチェックや言語などもっと絞り込む方法があると思います。
データを表示する際に、str_replace で置き換えていますが、DBには http://www.hoge.com
のように入れなくてはいけないのでしょうか?(下記の例では同じようにしていますが)
使い方が決まっているなら、登録時のチェックで処理した方が分かりやすいと思います。
(当然専用データとなってしまいますから、他に流用は出来なくなりますが)
また、このアドレスの場合表示されるのは、.hoge.com とドットから始まるのは想定通りですか?
https://abc.fuga.com/iwwwa/ のようなアドレスが入力される事は考えなくてもよいですか?
フォームを各行毎に入れるのではなく、テーブル全体を一つのフォーム内に収めました。
質問のスクリプトでは、どのボタンが押されたのかを判断できないとの理由で複数に分けたのだと予想しますが、こんな方法もあると思います。
日付(と思われる)は、UNIXタイムスタンプにしてみました。きっと経過時間などで使われるでしょうから、人間が読み易い値にしておく必要もないかと思って。
CURRENT_DATE() という関数もどのように定義されているか分かりませんので。
<?php // DBの情報 $dbname = 'db/link'; $db_default_table = ' id integer primary key, title text, category text, launguage text, link_ok text, url text, server_name text, uptime datetime'; // DBのオープン if ($db = sqlite_open($dbname, 0666, $sqliteerror)) { $result = @sqlite_query($db, 'SELECT * FROM site ORDER BY id DESC LIMIT 1', SQLITE_ASSOC); if (!$result){ // テーブルが存在しないと思われるので新規に作る(データも適当に追加する) sqlite_query($db, 'CREATE TABLE site ('.$db_default_table.')'); sqlite_query($db, 'INSERT INTO site VALUES (NULL, "タイトル", "カテゴリ1", "日本語", "リンクOK", "http://web.page.com", "サーバ名",'.time().')'); sqlite_query($db, 'INSERT INTO site VALUES (NULL, "たいとる", "カテゴリ2", "日本語", "リンクOK", "http://www.hogehoge.fuga.com", "server",'.time().')'); } }else{ exit($sqliteerror); } // データを削除する if ($key = array_search('削除', $_POST)) deleteData($db, $key); // データを追加する if (isset($_POST['add'])){ array_walk($_POST['new'], 'quotevalues'); insertData($db); } showAllData($db); sqlite_close($db); // データを追加 function insertData($d){ $query = 'INSERT INTO site ('; $values = 'VALUES ('; foreach ($_POST['new'] as $k=>$v){ $query .= $k.', '; $values .= '"'.$v.'", '; } $query .= 'uptime) '; $values .= time().')'; $res = sqlite_query($d, $query.$values); // 必要なら追加出来なかった場合の処理 } // 不適文字をエスケープ function quotevalues(&$value, $key){ $value = str_replace( "\0", '', $value); $value = sqlite_escape_string($value); } // データを削除 function deleteData($d, $k){ preg_match('/sub(\d+)/', $k, $id) or exit('削除キーがありません'); $res = sqlite_query($d, 'DELETE FROM site WHERE id = '.$id[1]); // 必要なら削除出来なかった場合の処理 return; } // 全データの表示 function showAllData($d){ $res = sqlite_unbuffered_query($d, 'SELECT * FROM site ORDER BY id DESC', SQLITE_ASSOC); // 項目名 print '<tr>' .'<td>id</td>' .'<td></td>' // 空 .'<td>題名</td>' .'<td>分類</td>' .'<td>言語</td>' .'<td>サイト名</td>' .'<td>URL</td>' .'<td>リンク</td>' .'<td>日付</td>' .'</tr>'."\n"; //データを表示する $lastID = 1; while ($row = sqlite_fetch_array($res, SQLITE_ASSOC)){ print '<tr>' .'<td>'.$row['id'].'</td>' .'<td><input type="submit" value="削除" name="sub'.$row['id'].'"></td>' //削除用ボタン .'<td>'.htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8').'</td>' .'<td>'.htmlspecialchars($row['category'], ENT_QUOTES, 'UTF-8').'</td>' .'<td>'.htmlspecialchars($row['launguage'], ENT_QUOTES, 'UTF-8').'</td>' .'<td>'.htmlspecialchars($row['server_name'], ENT_QUOTES, 'UTF-8').'</td>' .'<td>'.htmlspecialchars(preg_replace('!(http://|www)!', '', $row['url']), ENT_QUOTES, 'UTF-8').'</td>' .'<td>'.htmlspecialchars($row['link_ok'], ENT_QUOTES, 'UTF-8').'</td>' .'<td>'.htmlspecialchars(date('Y/m/d', $row['uptime']), ENT_QUOTES, 'UTF-8').'</td>' .'</tr>'."\n"; $lastID++; } // 追加用の入力フィールド print '<tr><td>'.$lastID.'</td>' .'<td><input type="submit" value="追加" name="add"></td>' .'<td><input type="text" name="new[title]"></td>' .'<td><input type="text" name="new[category]"></td>' .'<td><input type="text" name="new[launguage]"></td>' .'<td><input type="text" name="new[server_name]"></td>' .'<td><input type="text" name="new[url]"></td>' .'<td><input type="text" name="new[link_ok]"></td>' .'</tr>'."\n"; } ?>
ご回答ありがとうございます。
すみません。全部作っていただけるとは思いませんでした。
わざわざありがとうございます。
大変参考になりました。
>うまく削除できません。
削除ボタンを押した場合、hiddenでの名前が"del"になっているためです。ので"del"を"id"にすればokでしょう。ただ<form>が閉じていないので正しいidが戻っていません。</form>で閉じることで正しいidは戻りますが$_POST["title"]が設定されていないと
if (!isset($_POST["title"])){
の部分が実行されて終了していました。
>$query = "insert into site (title,category,launguage,link_ok,url,server_name,uptime)";
>この部分の項目が増減してもいちいち書き直さないでも良いようにしたいと言うことです。
php側だけではなくMySQL側でも直さなくてはならないのでその都度(頻繁に変更があることの方が問題)、書き換えでもいいと思います。
あるいは新たに追加がある場合はその部分だけのupdate文で処理するようにするなど。
エラー処理はやっかいです。ユーザーには詳細な内容を知らせない方がいいですし、自分には詳細な内容が必要です。ある程度の記述が必要ですのでset_error_handler()が便利です。後はエラーが発生したらtrigger_error()を呼ぶだけでset_error_handler()で指定した関数へ飛びます。
phpはhtml文の中に混在して書き込めるのが特徴の一つです。単純な場合はそれでいいのですが、複雑になるとこれが逆に足かせになってきます。今回は更にSQL文もあるので三つのものが混在しています。
提案としてはこれらはなるべく別々にするようにし、それらをつなぐものとして配列を使います。
$_POST,$_GET,$_COOKIE,$_SERVERなどのPHPの外部から来る変数は信用できません。
ので使用には常に注意が必要です。常に言えることは直接使ってはダメということです。
SQLインジェクションについて記憶に新しいものとしてはトレンドマイクロがあります。
http://jp.trendmicro.com/jp/about/notice/0312/index.html
考え方として攻撃を無害化するというのがmysql_real_escape_stringに代表されるものでしょう。
私の場合は攻撃を感知するということに重点を置いています。どういう攻撃があるかがわからないと感知できないのが難点ですが。
考え方が違えば、やり方も変わってきますし、どれが正解ということはないと思います。ようは攻撃を防げればいいのですから。
prepared文はMySQL 4.1から追加されたようです
http://lists.mysql.com/mysql-ja/77
これを使えば、あらかじめquery文を評価してしまい、後でexecute文で引数で置き換えて実行するというものです。
ので外部から与えられるものは単なるデータになるのでSQLインジェクションにはならないと理解しています(多分)。
プリペアドステートメントに渡すパラメータは、引用符で括る必要は ありません。それはドライバが自動的に行います。 アプリケーションで明示的にプリペアドステートメントを使用するように すれば、SQL インジェクションは決して発生しません (しかし、もし信頼できない入力をもとにクエリの他の部分を構築している のならば、その部分に対するリスクを負うことになります)。
mysql_xxxでは使えないようなのですがPEAR::DBなら使えます(多少オーバーヘッドは発生するでしょうが)。
php5ではPEAR::DBがうまく動かなかったのでPDOで実装しました。mysqliが改良版拡張サポートということでprepared文が使えるのですが使いかたが面倒そうなので断念しました。
<?php define('CR', "\n"); define('BR', "<br>"); define('DB_NAME', 'link'); define('USER', 'xxxxxx'); define('PASSWD', 'xxxxxxx'); //date_default_timezone_set('Asia/Tokyo'); function error_handler($errno, $errstr, $file, $line, $context) { static $errortype = array ( E_ERROR => 'Error', E_WARNING => 'Warning', E_PARSE => 'Parsing Error', E_NOTICE => 'Notice', E_CORE_ERROR => 'Core Error', E_CORE_WARNING => 'Core Warning', E_COMPILE_ERROR => 'Compile Error', E_COMPILE_WARNING => 'Compile Warning', E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', E_STRICT => 'Runtime Notice', E_RECOVERABLE_ERROR => 'Catchable Fatal Error'); echo $errortype["$errno"].':'.$file.':'.$line.'<br>'.$errstr.'<br>'.'<br>'; return TRUE; } function mysql_prepared($sql, $parms=NULL) { $mysql = new PDO('mysql:host=localhost;dbname='.DB_NAME, USER, PASSWD); $stmt = $mysql->prepare($sql); $stmt->execute($parms); return $stmt->fetchAll(PDO::FETCH_ASSOC); } function mysql_insert($parms) { $sql = "insert into site (title, category, launguage, link_ok, url, server_name, uptime)". " values (?, ?, ?, ?, ?, ?, CURRENT_DATE())"; return mysql_prepared($sql, $parms); } function mysql_delete($parms) { $sql = "DELETE FROM site WHERE id = ?"; return mysql_prepared($sql, $parms); } function table_tr_html($attr, $tr_ary) { $contents = ''; foreach ($tr_ary as $col) $contents .= '<td>'.$col.'</td>'; return '<tr'.$attr.'>'.$contents.'</tr>'; } function table_html($table_ary) { $contents = table_tr_html(' bgcolor="#88bbff" height="32"', array_shift($table_ary)); foreach ($table_ary as $id=>$row) $contents .= table_tr_html($id%2 ? ' bgcolor="#fafaff"':'', $row); return '<table>'.$contents.'</table>'; } function db_table_html($db_data) { $map = array(); foreach ($db_data as $row) $map[] = array( $row['id'], BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'. '<input type="submit" name="op" value="削除">'. '<input type="hidden" name="id" value="'.$row['id'].'">'.'</form>', $row['title'], $row['category'], $row['launguage'], $row['link_ok'], str_replace(array('http://','www.'),array('',''),$row['url']), $row['server_name'], date('Ymd',strtotime($row['uptime']))); return $map; } function validate($vars, $request) { $parms = array(); foreach ($request as $name) if (isset($vars["$name"])) $parms["$name"] = htmlspecialchars($vars["$name"]); return $parms; } set_error_handler("error_handler"); $request = array('id','','title','category','launguage','link_ok','url','server_name'); $parms = validate($_POST, $request); if ($parms && isset($_POST['op'])) { if ($_POST['op'] === '新規') mysql_insert(array_values($parms)); else if ($_POST['op'] === '削除') mysql_delete(array($parms['id'])); } $db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC"); $db_table = array_merge(array(array_merge($request, array('uptime'))), db_table_html($db_all_data), array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'. '<input type="submit" name="op" value="新規">', '<input type="text" name="title" value="タイトル">', '<input type="text" name="category" value="カテゴリー">', '<input type="text" name="launguage" value="言語">', '<input type="text" name="link_ok" value="リンク">', '<input type="text" name="url" value="url">', '<input type="text" name="server_name" value="サーバー">'.'</form>',''))); restore_error_handler(); ?> <html> <head> </head> <body> <?= table_html($db_table) ?> </body> </html>
動作確認はphp5、mysql4.1以上でdefineのUSER,PASSWDを設定してあれば動くとは思います。
error_handler()
set_error_handler("error_handler")としているのでエラーがあればここに飛んできます。そのまま表示していますのでユーザーには別のメッセージを表示して、自分にはメールを飛ばすようにするなどにした方がいいでしょう。
mysql_prepared()
prepared文を使ってquery()します。
mysql_insert($parms)
mysql_prepared()使って挿入します。挿入する内容についてはチェックしていません。prepared文を使っているのでSQLインジェクションの問題やhtmlspecialchars()が施されているので大まかな問題は解決されていると思いますが、あまりにも長すぎるとか不適切な内容などについては調べる必要があるでしょう。
mysql_delete($parms)
mysql_prepared()使って削除します。idについてはチェックしていませんが不適切なidなら削除されないだけなので実害は無いという判断です。
table_tr_html($attr, $tr_ary)
htmlの<tr>..</tr>の部分を生成します。
table_html($table_ary)
htmlの<table>..</table>の部分を生成します。
db_table_html($db_data)
dbの内容(二次元)をhtmlに変換する前段階として各要素を多少変更します。また削除のボタンを追加しています。
validate($vars, $request)
セキュリティの要としています。この部分をパスするということは処理を続行しても問題無い、あるいは大きな問題は無いと判断したということにしています。今回はhtmlspecialchars()だけしていますが、$requestで受け取るべき名前を列挙しているので、足りなかったり、逆に$requestにないものが$_POSTにあったら処理を止めるなどが可能です。
以下がメインの処理です。
set_error_handler("error_handler");
"error_handler"を設定します。エラーや問題を検知した場合はtrigger_error("メッセージ", E_USER_ERROR)で通知するようにしています。
$request = array('id','','title','category','launguage','link_ok','url','server_name');
$_POSTで受け取る名前を列挙します(テーブルのヘッダーとしても利用しているので途中に空の要素を入れていますが)。
$parms = validate($_POST, $request);
$_POSTの中で$requestで列挙されたものだけhtmlspecialchars()して$parmsに取り出します。
if ($parms && isset($_POST['op'])) {
if ($_POST['op'] === '新規')
mysql_insert(array_values($parms));
else if ($_POST['op'] === '削除')
mysql_delete(array($parms['id']));
}
ボタン用に"op"という名前を追加しています。本来"op"も$requestの中に入れて$parms['op']とすべきですが多少処理が長くなるので$_POSTを直接使っています。単純に新規(追加)、削除をしていますが追加の場合は、重複しないかチェックするなどの処理が必要かもしれません。新規の場合、prepared文の?と$parmsの数が合ってないとエラーになってしまいます。<input>でテキストが空の場合、送られないと思うので、それがvalidate()で処理されず、数が合わなくなってしまいます(既知のバグ)。
$db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC");
データを全て取り込む。
$db_table = array_merge(array(array_merge($request, array('uptime'))),
db_table_html($db_all_data),
array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.
'<input type="submit" name="op" value="新規">',
'<input type="text" name="title" value="タイトル">',
'<input type="text" name="category" value="カテゴリー">',
'<input type="text" name="launguage" value="言語">',
'<input type="text" name="link_ok" value="リンク">',
'<input type="text" name="url" value="url">',
'<input type="text" name="server_name" value="サーバー">'.'</form>','')));
テーブル用にヘッダー用($requestに'uptime'を追加)、dbからのデータ、新規用の<input>をマージして置きます。
最終的にでhtmlに展開されます。
restore_error_handler();
エラーハンドラーを元に戻します。
ご回答ありがとうございます。
完全に動く物と丁寧な説明ありがとうございます。
私には難しいことがかなりありましたので、それを調べるのに時間が必要でした。
お返事遅れて済みません。
ただ、私には高度すぎて理解できないことも多く、下記の点で困っています。
あまりに基本的で大変恐縮なのですが、
お時間のあるときにでもお答えいただければと思います。
質問①
新規ボタンを押すとちゃんとデータが追加するのですが、
別ページに作成したフォームからは追加されません。
エラーなどは出ませんが、追加されずただ表示されるだけです。
このスクリプトをどのように変更したらよいのでしょうか?
ひょっとしたら項目を追加したりしていじってる内に
別ページから登録できなくなったのかもしれませんので、
このスクリプトで問題なければその旨お知らせください。
質問②
表の1行1行が高いのでこれを低くしたいです。
「height="32"」の数値を小さくしたのですが、
最上段が狭まるだけで他は変わりません。どうしたら良いのでしょうか?
よろしくお願いします。
ご回答ありがとうございます。
完全に動く物と丁寧な説明ありがとうございます。
私には難しいことがかなりありましたので、それを調べるのに時間が必要でした。
お返事遅れて済みません。
ただ、私には高度すぎて理解できないことも多く、下記の点で困っています。
あまりに基本的で大変恐縮なのですが、
お時間のあるときにでもお答えいただければと思います。
質問①
新規ボタンを押すとちゃんとデータが追加するのですが、
別ページに作成したフォームからは追加されません。
エラーなどは出ませんが、追加されずただ表示されるだけです。
このスクリプトをどのように変更したらよいのでしょうか?
ひょっとしたら項目を追加したりしていじってる内に
別ページから登録できなくなったのかもしれませんので、
このスクリプトで問題なければその旨お知らせください。
質問②
表の1行1行が高いのでこれを低くしたいです。
「height="32"」の数値を小さくしたのですが、
最上段が狭まるだけで他は変わりません。どうしたら良いのでしょうか?
よろしくお願いします。