備品を従業員に割り当てるため、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 does not match the one 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としてログインします。新しいブラウザ・タブまたはブラウザ・ウィンドウで、次のファイルを開きます。
http://localhost/hack.html
表面的に、ページを閲覧するユーザーにとって、これは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 does not match the one sent
header('Location: index.php');
exit;
}
$equip = getcleanequip();
...
ac_add_one.phpのフォームには、生成されたクロスサイト・リクエスト・フォージェリ・トークンが非表示フィールドとして含まれています。この値もユーザー・セッションに格納されます。printcontent()のCSRFチェックは、発行されたフォーム内のトークンがPHPに保存されたセッション値と一致するかどうかをチェックします。
ファイルを保存しAnyCoアプリケーションを再度実行して、Administratorとしてログインします。新しいブラウザ・タブまたはブラウザ・ウィンドウで、次のファイルを開きます。
http://localhost/hack.html
「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トークン生成の実装が用意されています。