備品を従業員に割り当てるため、AnyCoアプリケーションにHTMLフォームを用意する必要があります。管理者はフォームを使用して備品を特定の従業員に割り当てることができます。
この章のトピックは、次のとおりです。
新規のPHPファイルac_add_one.php
を作成します。ファイルの初期の状態は次のようになります。
<?php /** * ac_add_one.php: Add one piece of equipment to an employee * @package Application */ session_start(); require('ac_db.inc.php'); require('ac_equip.inc.php'); $sess = new \Equipment\Session; $sess->getSession(); if (!isset($sess->username) ││ empty($sess->username) ││ !$sess->isPrivilegedUser() ││ (!isset($_GET['empid']) && !isset($_POST['empid']))) { header('Location: index.php'); exit; } $empid = (int) (isset($_GET['empid']) ? $_GET['empid'] : $_POST['empid']); $page = new \Equipment\Page; $page->printHeader("AnyCo Corp. Add Equipment"); $page->printMenu($sess->username, $sess->isPrivilegedUser()); printcontent($sess, $empid); $page->printFooter(); // Functions ?>
操作のプロセス・フローはindex.php
とほぼ同じです。最初にac_add_one.php
を実行したときにはHTML入力フォームが表示されます。ユーザーがフォームを発行すると、ac_add_one.php
が再び起動されて、データがデータベースに挿入されます。
この関数で必要となる権限には、$_GET['empid']
または$_POST['empid']
スーパーグローバルに従業員IDが設定されているかどうかのチェックが含まれます。ac_add_one.php
が最初にコールされたとき(ac_emp_list.php
のprintrecords()
を参照)に、従業員IDがURLパラメータとして渡され、$_GET
スーパーグローバルに格納されます。ac_add_one.php
内のフォーム(後述します)が発行されたときに、従業員識別子が$_POST
に格納されます。
printcontent()
関数をac_add_one.php
に追加します。
/** * Print the main body of the page * * @param Session $sess * @param integer $empid Employee identifier */ function printcontent($sess, $empid) { echo "<div id='content'>\n"; $db = new \Oracle\Db("Equipment", $sess->username); if (!isset($_POST['equip']) ││ empty($_POST['equip'])) { printform($sess, $db, $empid); } else { /* if (!isset($_POST['csrftoken']) ││ $_POST['csrftoken'] != $sess->csrftoken) { // the CSRF token they submitted doesn't match the one we sent header('Location: index.php'); exit; } */ $equip = getcleanequip(); if (empty($equip)) { printform($sess, $db, $empid); } else { doinsert($db, $equip, $empid); echo "<p>Added new equipment</p>"; echo '<a href="ac_show_equip.php?empid=' . $empid . '">Show Equipment</a>' . "\n"; } } echo "</div>"; // content }
printcontent()
関数にはロジックが含まれており、HTMLフォームを出力するのか、それともユーザーが入力したデータを挿入するのかを判断します。コメントアウトされたCSRFトークン・コードについては後述します。
さらに、ac_add_one.php
にprintform()
関数を追加します。
/** * Print the HTML form for entering new equipment * * @param Session $sess * @param Db $db * @param integer $empid Employee identifier */ function printform($sess, $db, $empid) { $empname = htmlspecialchars(getempname($db, $empid), ENT_NOQUOTES, 'UTF-8'); $empid = (int) $empid; $sess->setCsrfToken(); echo <<<EOF Add equipment for $empname <form method='post' action='${_SERVER["PHP_SELF"]}'> <div> Equipment name <input type="text" name="equip"><br> <input type="hidden" name="empid" value="$empid"> <input type="hidden" name="csrftoken" value="$sess->csrftoken"> <input type="submit" value="Submit"> </div> </form> EOF; }
注意: EOF; トークンは行の先頭に配置し、その後に空白が含まれないようにします。 |
この単純なフォームはユーザーに値の入力を要求します。CSRFトークンについては後述します。
getcleanequip()
関数をac_add_one.php
に追加します。
/** * Perform validation and data cleaning so empty strings are not inserted * * @return string The new data to enter */ function getcleanequip() { if (!isset($_POST['equip'])) { return null; } else { $equip = $_POST['equip']; return(trim($equip)); } }
この実装では、入力されたデータの先頭または末尾の空白はすべて除去されます。
基本的なWebアプリケーションのセキュリティとして、入力をフィルタして出力をエスケープするということが一般的に言われています。getcleanequip()
関数は入力をフィルタします。ここで、他の方法でデータをサニタイズすることも可能です。HTMLタグを受け入れないようにすることもできます。このようなタグはPHPの入力フィルタを使用して除去します。たとえば、次のように変更できます。
$equip = $_POST['equip'];
変更後
$equip = filter_input(INPUT_POST, 'equip', FILTER_SANITIZE_STRING);
これによりHTMLタグが削除され、他のテキストが残ります。
ac_add_one.php
で、有効なデータがdoinsert()
で挿入されます。この関数のコードをファイルに追加します。
/** * Insert a piece of equipment for an employee * * @param Db $db * @param string $equip Name of equipment to insert * @param string $empid Employee identifier */ function doinsert($db, $equip, $empid) { $sql = "INSERT INTO equipment (employee_id, equip_name) VALUES (:ei, :nm)"; $db->execute($sql, "Insert Equipment", array(array("ei", $empid, -1), array("nm", $equip, -1))); }
これは一般的なバインド変数構文を使用して、ac_db.inc.php
内の既存のDb::execute()
メソッドを使用します。「Dbクラスを使用したSQLの実行」の説明のとおり、oci_execute()
がコールされるたびにDb
クラスは自動的にコミットされます。
最後に、ヘルパー関数getempname()
を追加してac_add_one.php
を完成させます。
/** * Get an Employee Name * * @param Db $db * @param integer $empid * @return string An employee name */ function getempname($db, $empid) { $sql = "SELECT first_name ││ ' ' ││ last_name AS emp_name FROM employees WHERE employee_id = :id"; $res = $db->execFetchAll($sql, "Get EName", array(array("id", $empid, -1))); $empname = $res[0]['EMP_NAME']; return($empname); }
これは、ac_show_equip.php
内の同名の関数と同じです。
ac_show_equip.php
フォームと同様の機能を使用して、レコードの削除や更新を実行できます。ただし、行をあるHTMLページでロックして別のページで変更できないという、ステートレスWebアーキテクチャの制限があります。
AnyCoアプリケーションを実行し、Administrator
としてログインします。「Steven King」の横の「Add One」リンクをクリックします。備品入力フォームが表示されます。
新規の備品paper
を入力し、「Submit」をクリックします。新規データが挿入されます。更新されたリストは、「Steven King」の横にある「Show」リンクをクリックすると表示されます。
現状のフォームはCSRF攻撃を受けやすい状態にあり、ログイン中のユーザーを他のサイトが悪用して、データの発行や、権限が必要な操作をユーザーに実行させる可能性があります。
これを再現するため、hack.html
という新規のHTMLページを作成します。
<html> <!-- hack.html: Show issues with CSRF --> <body> <h1>Make Millions!</h1> <form method='post' action='http://localhost/ac_add_one.php'> <div> Do you dream of being rich?<br> <input type="hidden" name="equip" value="fish"> <input type="hidden" name="empid" value="100"> <input type="submit" value="Win"> </div> </form> </body> </html>
使用するシステムに合わせて、HTMLフォームのactionのURLを変更します。
ブラウザでAnyCoアプリケーションを実行し、Administrator
としてログインします。新しいブラウザ・タブまたはブラウザ・ウィンドウで、次のファイルを開きます。
表面的に、ページを閲覧するユーザーにとって、これはAnyCoアプリケーションと何の関係もありません。
「Win」ボタンをクリックします。AnyCoアプリケーションがコールされ、架空の備品名fish
が、従業員100
(Steven King)の備品リストに挿入されます。挿入された値は、後続の「Show Equipment」ページに表示されます。
ac_add_one.php
を編集し、printcontent()
のチェックのコメントを削除してCSRF保護を有効にします。
... } else { if (!isset($_POST['csrftoken']) ││ $_POST['csrftoken'] != $sess->csrftoken) { // the CSRF token they submitted doesn't match the one we sent header('Location: index.php'); exit; } $equip = getcleanequip(); ...
ac_add_one.php
のフォームには、生成されたクロスサイト・リクエスト・フォージェリ・トークンが非表示フィールドとして含まれています。この値もユーザー・セッションに格納されます。printcontent()
のCSRFチェックは、発行されたフォーム内のトークンがPHPに保存されたセッション値と一致するかどうかをチェックします。
ファイルを保存しAnyCoアプリケーションを再度実行して、Administrator
としてログインします。新しいブラウザ・タブまたはブラウザ・ウィンドウで、次のファイルを開きます。
「Win」をクリックします。
今回は、printcontent()
内のCSRF保護が、発行されたフォーム内でCSRFトークンを検出しません。ログイン・ページindex.phpにリダイレクト後、ログアウトされます。AnyCoアプリケーションに再度ログインし、Steven Kingの備品リストが変更されておらず、fish
にかわる第2のエントリがないことを確認します。hack.htmlが正常に動作するには、ac_add_one.php
で正規の入力フォームが生成された際PHPセッション内に保存される、csrftoken
フィールドの値が必要になります。
CSRF保護は、Webアプリケーションに施す必要のある様々のキュリティ制限の1つにすぎません。Webにデプロイするコードは徹底的なセキュリティ評価を実施する必要があります。
広く使用されているPHPフレームワークの多くには、安全なアプリケーションの作成作業を省力化する機能が備わっています。たとえば、AnyCoのSessionクラスよりも安全なCSRFトークン生成の実装が用意されています。