ヘッダーをスキップ
Oracle® Database Express Edition 2日でPHP開発者ガイド
11g リリース 2 (11.2)
B66464-01
  目次へ移動
目次
索引へ移動
索引

前
 
次
 

9 データの挿入

備品を従業員に割り当てるため、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.phpprintrecords()を参照)に、従業員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.phpprintform()関数を追加します。

/**
 * 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アーキテクチャの制限があります。

1件挿入フォームの実行

AnyCoアプリケーションを実行し、Administratorとしてログインします。「Steven King」の横の「Add One」リンクをクリックします。備品入力フォームが表示されます。

挿入フォーム

新規の備品paperを入力し、「Submit」をクリックします。新規データが挿入されます。更新されたリストは、「Steven King」の横にある「Show」リンクをクリックすると表示されます。

備品の表示

ac_add_one.phpでのCSRFの例

現状のフォームは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アプリケーションと何の関係もありません。

CSRF

「Win」ボタンをクリックします。AnyCoアプリケーションがコールされ、架空の備品名fishが、従業員100(Steven King)の備品リストに挿入されます。挿入された値は、後続の「Show Equipment」ページに表示されます。

エントリfishを使用したときの「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としてログインします。新しいブラウザ・タブまたはブラウザ・ウィンドウで、次のファイルを開きます。

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トークン生成の実装が用意されています。