サンプルWEBアプリケーションのダウンロード
簡単な名簿管理システムを準備しました。sample_dbをダウンロードしてください。
ダウンロードしたフォルダを解凍 → フォルダHomepage内に移動
XAMPPを起動 → ブラウザでURI http://localhost/Homepage/sample_db/member/ を指定すると、名簿テーブルの全レコードが表示されます。検索・新規作成で、既存レコードの削除・修正、新規レコードの追加ができます。
サンプルシステムの流れは以下の通りです。
名簿データベースは、前章で作成したものです。
会員番号の重複チェックや追加・修正内容のブランクチェックなどしています。
スクリプトの簡単な解説
以下各スクリプトを簡単に解説します。
名簿テーブルの全数表示(index.php)
名簿テーブルの全レコードを読み込み、テーブル形式で表示します。
テーブル(データベース)の全数読み込みは、次の流れになります。
- sqliteに接続する
今回はPDO 関数を利用して接続します。PDO関数は、データベースへの共通化されたアクセス方法です。データベースの種類には依存しませんので、将来データベースを変更する(例 sqlite → mysql)場合にも簡単に変更でいます。 - SQLを文字列として作成する
meiboテーブルから全レコードを抽出、抽出順序は no(会員番号)の昇順です。
"SELECT * FROM meibo ORDER BY no ASC" - 作成したSQL文を実行する
今回は、prepare()とexecute()ステートメント(メソッドと呼ばれます)のセットで実行します。セキュリティ対策上このセットの利用が望ましいようです。 - レコードを取得する
fetchALL()ステートメントで、すべてのレコードを2次元配列$itemに読み込みます。1番目のレコードの会員番号は、$item[0][no]で得ることができます。
fetchALL()では、一度にすべてのレコードを配列に入れますので、レコード数が少ない場合にのみ利用してください。多い場合には、フリカナからの検索(search.php)をご覧ください。 - sqliteの接続を閉じる
nullを指定します。
<ソース>
<?php
/* データベース kaiin.db に接続します。 */
$dbh = new PDO("sqlite:../sqlite3/kaiin.db",null,null);
/* meiboテーブルのデータ全件を取り出します。
会員番号順に表示します。
*/
$sql = "SELECT * FROM meibo ORDER BY no ASC";
/* 実行するSQL文を準備します。 */
$sth = $dbh->prepare($sql);
/* SQL文を実行します。*/
$sth->execute();
/* 全てのレコードを配列$itemに取り込みます。
$item[0][no] 1番目のレコードのカラムnoの値です。
*/
$item = $sth->fetchAll();
/* レコード件数を$countにセットします。*/
$count = count($item);
/* データベースとの接続を閉じます。 */
$dbh = null;
?>
データベースkaiin.dbからすべてのレコードを読み込み、配列 $item にセットします。../sqlite3/kaiin.db でデータベースを指示しています。(ここの指定は、index.phpがあるフォルダの2階層上にあるフォルダsqlite3にデータベースkaiin.dbがあります) レコード数は、$count にあります。
/* */ コメントです。
これらのステートメントの実行中は、ブラウザには何も表示されていません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>簡易名簿データベースの管理</title>
</head>
<body>
ここまでで、htmlを指定しています。このステートメントはブラウザが解釈します。
<?php
/* データベースにレコードがある場合には、全てのレコードを表示します。 */
if ($count > 0) {
echo "<h3>名簿テーブルの全数を表示します。</h3>";
echo "<table border='1'>";
echo "<tr>";
echo "<td>会員番号</td><td>氏名</td><td>読み仮名</td><td>電話番号</td>";
echo "</tr>";
/* 取り出した各レコードをテーブルの1行にセットします。 */
for ($i = 0; $i < $count ; $i++) {
echo "<tr>";
echo "<td>".$item[$i]["no"]."</td>";
echo "<td>".$item[$i]["name"]."</td>";
echo "<td>".$item[$i]["kana"]."</td>";
echo "<td>".$item[$i]["tel"]."</td>";
echo "</tr>";
}
echo "</table>";
}
else {echo "<h3>表示するレコードはありません。</h3>";}
?>
echoでhtmlを出力しています。ブラウザは、この出力内容をもとに画面に表示します。
echo "<td>".$item[$i][no]."</td>"を実行すると、<td>$i番目の会員番号</TD>のイメージがブラウザに送られます。.(ピリオド)は、文字列をつなぐための演算子です。
<p><a href="form_search.html">検索</a></p>
<p><a href="form_input.html">新規追加</a></p>
</body>
</html>
phpスクリプトは、
- データベースの読み込み
- ブラウザに表示するためのhtml文の作成
を行っています。
<?phpと?>で囲まれていない部分は、htmlとしてブラウザにそのまま渡され、画面に表示されます。
レコードの検索 検索文字列の入力(form_search.html)
カナ文字での検索画面を表示します。ここでは、phpは使用していません。
<ソース>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>レコードの検索</title>
</head>
<body>
<p>レコードの検索。フリカナの前方一致で検索。</p>
<form name="form_search" method="post" action="search.php">フリカナ: <input name="f_kana" type="text">
<br><br>
<input type="submit" name="submit" value="検索">
</form>
</body>
</html>
inputで、フリカナの入力を行い、search.phpを呼び出します。
レコードの検索 テーブルから検索(search.php)
最初に、form_searchで入力された文字列をチェックします。
$f_kana = $_REQUEST["f_kana"]・・・formのinput(データ名 name="f_kana"で指定)で入力された文字列が、変数$f_kanaにセットされます。$f_kanaがブランクの場合には、エラーメッセージを表示します。
データベースに接続後、入力されたカナ文字に一致(前方)するレコード件数を求めます。
SQL文・・・"SELECT count(*) FROM meibo WHERE kana LIKE '$f_kana%'"
count(*)でレコード件数を求めます。
LIKE '$f_kana%'・・・LIKEと%で、文字列の一部を指定します。この指定は、$f_kanaで始まるレコードを指定しています。SQL文内に文字列を記述する場合には、'$f_kana%'のように、'(シングルクオーテーション)で囲みます。
一致するレコードがあれば、レコードを読み込むためのSQL文を実行します。実際のレコードの読み込みは、htmlのbodyないで、テーブル表示します。
<ソース>
<?php
/* フォームより送信された値を取得します。 */
$f_kana = $_REQUEST["f_kana"];
/* フォームからのデータがない場合には、エラーメッセエージを表示します。*/
if (!$f_kana) {echo "<h3>検索キーがありません。</h3>";}
else {
/* データベースに接続します。 */
$dbh = new PDO("sqlite:../sqlite3/kaiin.db",null,null);
/* nameカラムをキーワードで前方一致検索して一致するレコード件数を取得します。 */
$sql = "SELECT count(*) FROM meibo WHERE kana LIKE '$f_kana%'";
/* 実行するSQL文を準備します。 */
$sth = $dbh->prepare($sql);
/* SQL文を実行します。 */
$sth->execute();
/*
* 結果の件数を取得します。
* PDOクラスのfetchColumn()は結果セットから単一(最初のカラム)の値を返します。
*/
$count = 0;
$count = $sth->fetchColumn();
/*
* 一致したレコードがあれば、そのレコードの内容を読み込みます。
*/
if($count > 0){
/* nameカラムをキーワードで前方一致検索してデータを取得します。 */
$sql = "SELECT * FROM meibo WHERE kana LIKE '$f_kana%'";
/* 実行するSQL文を準備します。 */
$sth = $dbh->prepare($sql);
/* SQL文を実行します。 */
$sth->execute();
}
}
?>
$countには、一致したレコードの件数。
$sthには、検索したレコードの結果があります。
検索した結果を表示するためのhtmlを出力します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>レコードの検索。氏名(カナ)の前方一致で検索</title>
</head>
<body>
<h3>レコードの検索。氏名(カナ)の前方一致で検索。</h3>
一致するレコードを順次読み込み、1レコードを1行として表示します。
echo "<td><a href='delete.php?f_no=$item[no]'>削除</a></td>"
[削除][編集]へのリンクを付けますが、その時、プログラム名に続いて?=で、リンク先のプログラムに、会員番号(no)をパラメータ($_GETデータ)として渡します。(削除・更新は、会員番号をもとに行います)
while($item = $sth->fetch())・・・1次元配列$itemにレコードを1件読み込みます。レコードがなくなるまで、繰り返します。レコード数が多いテーブルの全数表示の場合には、この繰り返し分を利用した方が効率的です。
<?php
/* 結果がある場合に検索結果を出力します。 */
if($count > 0){
echo "<table border='1'>";
echo "<tr><td>会員番号</td><td>氏名</td><td>フリガナ</td><td>電話番号</td><td colspan='2'>編集</td></tr>\n";
/*
* 結果を取り出します。
* カラム名をキーに値を取り出しています。
*/
while($item = $sth->fetch()){
echo "<tr>";
echo "<td>$item[no]</td>";
echo "<td>$item[name]</td>";
echo "<td>$item[kana]</td>";
echo "<td>$item[tel]</td>";
echo "<td><a href='delete.php?f_no=$item[no]'>削除</a></td>";
echo "<td><a href='edit.php?f_no=$item[no]'>編集</a></td>";
echo "</tr>";
}
echo "</table>";
}else{
echo "<h3>条件に一致するデータが見つかりませんでした。</h3>";
}
/* 接続を閉じます。 */
$dbh = null;
?>
<p><a href="index.php">戻る</a></p>
</body>
</html>
echo "<td><a href='delete.php?f_no=$item[no]'>削除</a></td>"
リンクされた文字列[削除]をクリックすると、delete.phpが呼び出されます。
echo "文字列" で文字列が表示されますが、文字列に"(ダブルクオーテーション)がある場合には、'(シングルクオーテーション)または\"(逆スラッシュは円記号 ¥)を使います。
指定された会員番号のレコードを削除(delete.php)
$_GET["f_no"]で削除する会員番号を取得することができます。
SQL文の実行は、$dbh->exec($sql)です。execは、削除して行数を返しますが、ここでは返り値は不要ですので、返り値をセットする変数を指定していません。prepare,executeを利用しても問題ありません。
<ソース>
<?php
/* フォームより送信された値を取得します。 */
$f_no = $_GET["f_no"];
/* データベースに接続します。 */
$dbh = new PDO("sqlite:../sqlite3/kaiin.db",null,null);
/* 指定されたidのデータを削除します。 */
$sql = "DELETE FROM meibo WHERE no = $f_no";
/* SQL文を実行します。*/
$dbh->exec($sql);
/* 接続を閉じます。 */
$dbh = null;
?>
$sql = "DELETE FROM meibo WHERE no = $f_no"
指定された会員番号(f_no)のレコードが削除されました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>指定した会員番号のレコードを削除</title>
</head>
<body>
<h3>レコードの削除。指定した会員番号のレコードを削除。</h3>
<p>レコードは削除されました。</p>
<p><a href="index.php">戻る</a></p>
</body>
</html>
処理終了後、最初(名簿テーブルの表示)に戻ります。
レコードの修正 修正内容の入力(edit.php)
会員番号のレコードを取得して、その内容を表示します。表示された内容を変更すれば会員レコードに反映されます。なお、会員番号は一番大事なキーですので、変更できないようにしています。
このプログラムでは、修正内容の取得のみ行い、レコードはupdate.phpで更新されます。
SQL文の実行は、$sth = $dbh->query($sql)です。queryは、SQLを実行して、結果を返します。(SELECTの場合には、読み込まれたレコードの内容が$sthにセットされます) prepare,executeを利用しても問題ありません。
<ソース>
<?php
/* フォームより送信された値を取得します。 */
$f_no = $_GET["f_no"];
/* データベースに接続します。 */
$dbh = new PDO("sqlite:../sqlite3/kaiin.db",null,null);
/* 指定されたidのデータを取り出します。 */
$sql = "SELECT * FROM meibo WHERE no = $f_no";
/* SQL文を実行します。*/
$sth = $dbh->query($sql);
/*
* 結果を1行取り出します。
* PDOStatementクラスのfetch()で引数にPDO::FETCH_OBJを指定した場合は、
* カラム名をプロパティとして値を取り出すことができます。
*/
$item = $sth->fetch(PDO::FETCH_OBJ);
$no = $item->no;
$name = $item->name;
$kana = $item->kana;
$tel = $item->tel;
?>
変数$no,$name,$kana,$telに、指定された会員の情報がセットされています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>名簿テーブルから取り出したレコードの修正</title>
</head>
<body>
<h3>名簿テーブルから取り出したレコードの修正。</h3>
<p>会員番号は変更できません</p>
<?php echo "<form name='form_edit' method='post' action='update.php?f_no=$no' "?>>
<h4>会員番号:<?php echo $no ?></h4>
名前: <input name="f_name" type="text" value="<?php echo $name ?>">
<br><br>
フリカナ: <input name="f_kana" type="text" value="<?php echo $kana ?>">
<br><br>
電話番号:<input name="f_tel" type="text" value="<?php echo $tel ?>">
<br><br>
<input type="submit" name="submit" value="修正">
</form>
</body>
</html>
<input name="f_name" type="text" value="<?php echo $name ?>">
姓名・・・名簿テーブルに登録されている氏名を表示し、変更が可能です。変更された内容は、続くプログラム(update.php)で取り出すことができます。html文の一部を<?php ?>で、実行時の条件により変更しています。
[修正]がクリックされると、update.phpが呼び出されます。
レコードの修整 テーブルの更新(update.php)
$_GETで、更新するレコードの会員番号を取得します。
$_POSTで、inputで入力された情報を取得します。$_POST["f_name"]・・・入力された氏名です。
氏名・カナ・電話番号に記入漏れがあれば、$errorにエラーサイン1をセットします。
<ソース>
<?php
/* フォームより送信された値を取得します。 */
$f_no = $_GET["f_no"];
$f_name = $_POST["f_name"];
$f_kana = $_POST["f_kana"];
$f_tel = $_POST["f_tel"];
/* どれか一つでも記入漏れがあればエラーにします。*/
$error = 0;
if ((!$f_no) || (!$f_name) || (!$f_kana) || (!$f_tel)) {$error = 1;}
else {
/* データベースに接続します。 */
$dbh = new PDO("sqlite:../sqlite3/kaiin.db",null,null);
/* 指定されたnoのデータを更新します。 */
$sql = "UPDATE meibo SET name = '$f_name',kana = '$f_kana',tel = '$f_tel' WHERE no = $f_no";
/* SQL文を実行します。 */
$dbh->exec($sql);
/* 接続を閉じます。 */
$dbh = null;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>名簿テーブルから取り出したレコードの更新</title>
</head>
<body>
<h3>名簿テーブルから取り出したレコードの更新。</h3>
<?php
if ($error == 0) {echo "<p>レコードを変更しました。</p>";}
else {echo "<p>記入漏れがあります。変更されませんでした。</p>";}
?>
<p><a href="index.php">戻る</a></p>
</body>
</html>
更新が終われば、最初(名簿テーブルの表示)に戻ります。
新規レコードの追加 追加内容の入力(form_input.html)
formで、新規レコードの情報を入力します。入力終了後、input.phpが呼び出されます。
phpは利用していません。
<ソース>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>名簿レコード追加</title>
</head>
<body>
<h3>ブラウザから名簿テーブルにレコードを追加。</h3>
<form name="form_input" method="post" action="input.php"><br>
会員番号: <input type="text" name="f_no">
<br><br>
名前 : <input type="text" size="40" name="f_name"> <br>
<br>
フリガナ : <input type="text" name="f_kana">
<br><br>
電話番号: <input type="text" name="f_tel">
<br><br>
<input type="submit" name="submit" value="追加">
</form>
</body>
</html>
[追加]がクリックされると、input.phpが呼び出されます。
新規レコードの追加 テーブルの更新(input.php)
form_input.htmlで入力された情報が新規レコードとしてテーブルに追加されます。
入力内容がブランクでないこと及び会員番号が重複していないことをチェックしています。
<ソース>
<?php
/* フォームより送信された値を取得します。 */
$f_no = $_POST["f_no"];
$f_name = $_POST["f_name"];
$f_kana = $_POST["f_kana"];
$f_tel = $_POST["f_tel"];
/* どれか一つでも記入漏れがあれば$errorに1をセットします。*/
$error = 0;
if ((!$f_no) || (!$f_name) || (!$f_kana) || (!$f_tel)) {$error = 1;echo "error";}
else {
/* データベースに接続します。 */
$dbh = new PDO("sqlite:../sqlite3/kaiin.db",null,null);
/* noが登録済みの場合には、エラーにします。*/
$sql = "SELECT count(*) FROM meibo WHERE no = $f_no";
/* 実行するSQL文を準備します。 */
$sth = $dbh->prepare($sql);
/* SQL文を実行します。*/
$sth->execute();
/* noが登録済みか確認します。重複していたら$errorに2をセットします。*/
$count = $sth->fetchColumn();
if ($count != 0) {$error = 2;}
else {
/* meiboテーブルにデータを追加します。 */
$sql = "INSERT INTO meibo (no,name,kana,tel) VALUES('$f_no','$f_name','$f_kana','$f_tel')";
/* 実行するSQL文を準備します。 */
$sth = $dbh->prepare($sql);
/* SQL文を実行します。 */
$sth->execute();
/* 接続を閉じます。 */
$dbh = null;
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>名簿テーブルにレコードの追加</title>
</head>
<body>
<h3>ブラウザから名簿テーブルにレコードを追加。</h3>
<?php if ($error == 1) {echo "<p>記入漏れがあります。</p>";}
elseif ($error == 2) {echo "<p>会員番号が重複しています。</p>";}
else {echo "<p>レコードが追加されました。</p>";}
?>
<p><a href="index.php">戻る</a></p>
</body>
</html>
更新が終われば、最初(名簿テーブルの表示)に戻ります。
このシステムで、名簿テーブルの管理ができることを実際に試してみてください。
このシステムは、自然の流れで実行していると、問題なく正常に機能しますが、セキュリティ対策は一切講じられていません。その結果、誤った使い方や悪意を持ったユーザーの利用を考えると、多くの問題点が見えてきます。(システムの脆弱性)この対策を適切に行っていないと、個人情報の漏えいなどが発生します。(たまに大手のサイトからの個人情報の漏えい問題が報じられますが、ほとんどの場合、システムの脆弱際対策が適切に行われていないのが漏えいした原因です)
ブラウザの更新ボタンの利用
途中でブラウザの更新ボタンをクリックすると、以下のメッセージが表示されます。
[再試行]をクリックすると、URIアドレスのプログラムが再実行されますので、追加・更新・削除の場合には、二重の処理が行われます。
このシステムでは、削除・更新時のチェック(例 削除の場合には、削除するレコードが存在することのチェック)をしていませんので、問題はありません。追加の場合には、会員番号の重複エラーとなります。
全数表示の場合には、二重処理されても問題はありません。
この例では、[再試行]の問題は少ないですが、商品の購入などの場合には二重購入となりますので、その対策をシステムの中に組み込んでおく必要があります。
入力画面で特殊な文字列が入力される その1(クロスサイトスクリプティング脆弱性)
ソフトの知識がある悪意を持ったユーザの対策です。
レコードの追加 追加内容の入力画面で、氏名欄に <script>alert("おはよう");</script> の文字列を入力してみてください。(他の項目は正しく)
[追加]して全数表示画面に戻ってください。全数表示画面の表示途中で、
が表示され、[OK]をクリックすると、全数表示されます。会員番号2001の氏名欄には入力内容は表示されません。
ブラウザでソーススクリプトを見てみましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>簡易名簿データベースの管理</title>
</head>
<body>
<h3>名簿テーブルの全数を表示します。</h3><table border='1'><tr><td>会員番号</td><td>氏名</td><td>読み仮名</td><td>電話番号</td></tr><tr><td>1101</td><td>名取 裕子</td><td>ナトリ ユウコ</td><td>3561-5581</td></tr><tr><td>1201</td><td>安藤 由紀子</td><td>アンドウ ユキコ</td><td>6534-1221</td></tr><tr><td>1202</td><td>市原 悦子</td><td>イチハラ エツコ</td><td>1531-6789</td></tr><tr><td>1205</td><td>東 ちづる</td><td>アズマ チヅル</td><td>5544-9981</td></tr><tr><td>2000</td><td>秋葉 敬三</td><td>アキバ ケイゾウ</td><td>5455-6780</td></tr><tr><td>2001</td><td><script>alert("おはよう");</script></td><td>オハヨウ</td><td>1234-1234</td></tr></table><p><a
href="form_search.html">検索</a></p>
<p><a href="form_input.html">新規追加</a></p>
</body>
</html>
会員番号2001の名前欄には、入力した内容がセットされています。
<script>から</script>には、java scriptと呼ばれるスクリプト(プログラム)が記述され、ブラウザはその内容を解釈して実行します。(alert("おはよう");はメッセージ「おはよう」を表示するスクリプトです)
この場合、PHPで入力されたタグがそのままhtmlに出力され、ブラウザが理解して実行されるようになっています。好きなタグを入力できますが、「悪意あるユーザ」はここを利用して、個人情報の抜き出しや他人への成りすまし・悪意あるサイトへの誘導などを仕掛けてきます。このような問題点(脆弱性)は、クロス・サイト・スクリプティング(XSS)脆弱性を呼ばれます。
入力画面で特殊な文字列が入力される その2(SQLインジェクション)
SQL文が入力され、その結果、設計者が想定していないデータベースの更新処理が行われてしまう場合があります。
以下の操作を行う前に、kaiin.dbのコピーをデスクトップ等に作成し、レコードが削除された場合に簡単に戻せるようにしておいてください。
”オハヨウ”でレコードを検索して、削除(delete.php)してください。
削除が完了すると、
が表示されます。
この画面表示のURIは、http://localhost/Homepage/lesson/lesson4/delete.php?f_no=2001
となっています。(f_no=削除した会員番号です)
このURIのf_no部分を以下に変更してブラウザの更新ボタンをクリックしてください。
f_no=2001 → f_no=1 or 1=1
削除完了のメッセージが表示されますので、[戻る]をクリックしてください。
「表示するレコードはありません」のメッセージが表示されます。(実は、テーブルの全レコードが削除されています。)
このようにf_no部分のSQL文が変更され、設計者が意図しない結果(テーブルから全レコードの削除)となっています。
テーブルからレコードの削除
delete.php内で設定されたSQL文は、"DELETE FROM meibo WHERE no = $f_no"です。
通常、$f_noには、削除するレコードの会員番号が設定されています。(この会員番号は、URIのf_no=でdelete.phpに渡されます。) 今回、URIが、f_no=1 or 1=1 と変更された結果、deleteのSQL文は、"DELETE FROM meibo WHERE no = 1 or 1=1"となり、orで指定したWHERE条件が常に成り立ちます。この結果、テーブルmeiboの全レコードが削除されます。(テーブルそのものは削除されません。レコードがないテーブルが残っています。)
今回は、URIのパラメータ($_GET)にSQL文の一部を追加して全レコードの削除を行いましたが、input文($_POST)に追加して、このような操作を加えることができます。
このように、SQL文を作成するとき単純に入力された文字列を結合すると、設計者が予想しないSQL文が作成されてしまい、テーブルの破壊や情報の漏えいなどが発生します。これがSQLインジェクション脆弱性です。
WEBアプリケーションの脆弱性全般
今回は、クロスサイトスクリプティング及びSQLインジェクションの二つの脆弱性を取り上げました。WEBアプリケーションを開発する場合には、最低この2つの脆弱性対策を行っておく必要があります。
完全なシステムのセキュリティ対策、データベースの安全対策には、その他の対策も必要となります。詳しい事例は、WEBアプリにおける11の脆弱性の常識と対策 をご覧ください。
これらの対策が適切に行うことができない場合には、個人情報(特に住所やクレジットカード番号など)はアプリケーションで扱わないようにしてください。
サンプルのWebアプリケーションに脆弱性対策を追加します。
入力画面のデータの妥当性チェックの追加
会員番号や氏名・フリカナ・電話番号を会員情報として入力・更新されるシステムですが、入力されたデータが正しい形式になっているかのチェックは行っていません。このような妥当性のチェックはシステムのセキュリティ対策として重要です。
このシステムでは、最低、以下の妥当性チェックをしておくべきでしょう。
カラム | 正しい形式 | 妥当性チェック |
---|---|---|
会員番号 | 4桁の数字 レコードの存在チェックは、データベース更新時に行います。 (新規追加 名簿テーブルに存在しない;更新 名簿テーブルに存在する) |
正規表現 [0-9]{4} |
名前 | 特に制限はない | スペースではない |
フリガナ | 全角カタカナのみ | 正規表現 ^[ア-ン]+$ |
電話番号 | 1桁以上4桁以内の数字-4桁の数字 (市外局番は使用しません。さらに厳密なチェックも可能ですが、 ここではこの程度にしておきます) |
正規表現 [0-9]{1,4}-[0-9]{4} |
このようなチェックは、ブラウザおよびphpで行うことができます。ブラウザでのチェックは入力ミスを防ぐため(善意のユーザーの利便性)、PHPでのチェックは悪意のユーザからのセキュリティ対策のためとなりますので、できるだけ二重のチェックを組み込んでください。
ブラウザでのチェックはjavascriptで行いますが、offにすることができますので、悪意のユーザからのセキュリティ対策にはなりません。
入力データのチェックの組み込み
正規表現を使った入力データのチェックは、ereg関数またはmb_ereg関数(日本語関数 フリガナのチェック)を利用します。
ereg(正規表現パターン、文字列) mb_ereg(正規表現パターン、文字列)
正規表現を満足している場合には、trueを返します。
<チェック部分のソーススクリプト(input.php update.php)>
/* mb_eregを使う際には、mb_regex_encoding()関数で正規表現内で使う文字エンコーディングを指定します。*/
mb_regex_encoding("utf-8"); // UTF-8
/* どれか一つでも記入漏れや内容に誤りがあれば$errorに1をセットします。*/
$error = 0;
if ((!$f_no) || (!$f_name) || (!$f_kana) || (!$f_tel)) {$error = 1;}
/* no,kana,telのチェックをします。正しくなければ$errorに1をセットします */
if (!ereg("[0-9]{4}", $f_no)) {$error = 1;}
if (!mb_ereg("[ア-ン]+$",$f_kana)) {$error = 1;}
if (!ereg("[0-9]{1,4}-[0-9]{4}", $f_tel)) {$error = 1;}
mb_regex_encoding関数での文字エンコーディング指定・・・PHP5.4では不要です。
クロス・サイト・スクリプティング脆弱性対策
この脆弱性は、タグが入力されたときそのままhtmlに出力されブラウザが解釈するできるところに問題があります。タグが入力されても、ブラウザがタグとして解釈できないようにしておけば、この脆弱性を防ぐことができます。
htmlには、<などの特殊文字を、<のような単なる文字列で表すことができます。(エスケープ処理とかサニタイジング・・・無害化・・・と呼ばれます)htmlに出力するとき、タグとして機能する特殊文字(以下の5文字)をエスケープ処理をしておくと、この脆弱性は防ぐことができます。
エスケープする文字列 | エスケープ後の文字列 |
---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
PHPでは、htmlspecialchars関数で、この5文字のエスケープ処理を行うことができます。
htmlspacialchars関数
htmlspecialachars(文字列,ENT_QUOTES)
文字列:出力する文字列
ENT_QUOTES:シングルクオーテーション(')、ダブルクオーテーション(")をエスケープ処理する指定
クロス・サイト・スクリプティング脆弱性対策を講じたPHPスクリプト
名簿テーブルの全数表示(index.php)
boby部分のソーススクリプト 赤字部分を修正
<body>
<?php
/* データベースにレコードがある場合には、全てのレコードを表示します。 */
if ($count > 0) {
echo "<h3>名簿テーブルの全数を表示します。</h3>";
echo "<table border='1'>";
echo "<tr>";
echo "<td>会員番号</td><td>氏名</td><td>読み仮名</td><td>電話番号</td>";
echo "</tr>";
/* 取り出した各レコードをテーブルの1行にセットします。 */
for ($i = 0; $i < $count ; $i++) {
echo "<tr>";
echo "<td>".htmlspecialchars($item[$i][no],ENT_QUOTES)."</td>";
echo "<td>".htmlspecialchars($item[$i][name],ENT_QUOTES)."</td>";
echo "<td>".htmlspecialchars($item[$i][kana],ENT_QUOTES)."</td>";
echo "<td>".htmlspecialchars($item[$i][tel],ENT_QUOTES)."</td>";
echo "</tr>";
}
echo "</table>";
}
else {echo "<h3>表示するレコードはありません。</h3>";}
?>
<p><a href="form_search.html">検索</a></p>
<p><a href="form_input.html">新規追加</a></p>
</body>
出力されたhtmlのソーススクリプト
<body>
<h3>名簿テーブルの全数を表示します。</h3><table border='1'><tr><td>会員番号</td><td>氏名</td><td>読み仮名</td><td>電話番号</td></tr><tr><td>1101</td><td>名取 裕子</td><td>ナトリ ユウコ</td><td>3561-5581</td></tr><tr><td>1201</td><td>安藤 由紀子</td><td>アンドウ ユキコ</td><td>6534-1221</td></tr><tr><td>1202</td><td>市原 悦子</td><td>イチハラ エツコ</td><td>1531-6789</td></tr><tr><td>1205</td><td>東 ちづる</td><td>アズマ チヅル</td><td>5544-9981</td></tr><tr><td>2000</td><td>秋葉 敬三</td><td>アキバ ケイゾウ</td><td>5455-6780</td></tr><tr><td>2001</td><td><script>alert("おはよう");</script></td><td>オハヨウ</td><td>1234-5678</td></tr></table><p><a
href="form_search.html">検索</a></p>
<p><a href="form_input.html">新規追加</a></p>
</body>
同様な修正を、レコードの検索(search.php)及びレコードの修整(edit.php)に追加してください。
htmlspecialchars関数は、htmlに出力するとき、1回だけ適用してください。
今回は、名簿テーブルの全カラムに対してエスケープ関数を適用していますが、入力データの妥当性チェックを行えば、名前欄のみに適用すれば十分です。
SQLインジェクション脆弱性対策
文字列を単純に結合するときの問題ですから、結合する文字列がSQLとして意味を持たない文字列に変更すれば回避することができます。(特殊文字のエスケープ処理)
対策・・・プリペアドステートメントを使う
ここではPDO関数を利用していますので、プリペアドステートメントでインジェクション脆弱性対策を講じます。
プリペアドステートメントは、データベースへのアクセスの前に、SQL文を実行可能な形式に変換(?部分は除く)し、実行時に?部分に、与えられたパラメータを変更できるようにしておきます。(仕組みの詳細は省略)
このステートメントを使うと、SQLインジェクションを防ぐことができます。
プリペアドステートメントの設定
SQL文の作成と実行を以下のように変えます。
/* 実行するSQL文を準備します。 */
$sql = "DELETE FROM meibo WHERE no = ?";
/* プリペアドステートメントを準備します */
$sth = $dbh->prepare($sql);
/* 可変項目を配列にセットします。 */
$temp = array($f_no);
/* SQL文を実行します。 */
$sth->execute($temp);
可変項目が複数ある場合には、?をそれぞれ指定し、その順番に値がセットされた配列($temp)を作り、excecute文で指定します。
データベースを更新する全てのプログラム(input.php,update.php,delete.php)に、プリペアドステートメントを追加しました。
これで最低限必要な脆弱性対策が完了しました。
脆弱性対策を講じた名簿管理システム
出来上がったアプリは、フォルダsample_db内のrevisedにあります。
ブラウザで、URI http://localhost/Homepage/sample_db/revised/ を指定して確認してください。
2016.12