【ドットインストール】PHPで作る「簡易掲示版」の解説

完成図;簡易掲示板

ドットインストールのレッスン、「PHPで作る「簡易掲示版」 (全9回)」の備忘録です。復習用にお使いください。

簡易掲示板の仕組みはDBを使わずにdatファイルを使っているので、それほど難しくはありません。しかし、CSRF対策というものがわからない方がいると思います。今回はCSRFとは何かということを具体例をあげて解説しています。

また、CSRF対策を通じてハッシュについても軽く触れています。メッセージダイジェストで調べてみると理解が深まるかもしれません。

datファイルへ入力値(form)を書き込む #03

#03 データを書き込んでみよう

入力値の保存の流れの以下の通りです。

  1. formに入力値が送信される。
  2. 現ページにPOST送信
  3. 入力値を加工してdatファイルに書き込む

図:入力値の保存の流れ

流れは簡単ですが、ここで初めてif文について少しメモ。$_SERVERを初めて見る方もいると思いますので。

// リクエストのメソッドを判断
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    // 書き込み処理 
}

$_SERVER

ヘッダ、パス、スクリプトの位置のような 情報を有する配列

この配列から色々な情報を引き出すことが出来ます。以下基本的なインデックスです。

定数 意味
REQUEST_METHOD ページにアクセスする際に使用されたリクエストのメソッド
PHP_SELF 現在実行しているスクリプトのファイル名
SCRIPT_NAME 現在のスクリプトのパス
SCRIPT_FILENAME 現在実行されているスクリプト絶対パス
argv スクリプトに渡された引数の配列
REMOTE_USER 認証されたユーザー

今回のソースではREQUEST_METHODを使用しています。これは'GET', 'HEAD', 'POST', 'PUT'などが返ってくるので、その文字列と等しいかで、リクエスト、つまりformで指定されたmethodが何かが判断できます。

ファイルの書き込みは以上です。最後に拡張datについて少し触れておきます。

DATファイル

“dat”は「データ」(data)の略号で、プログラムなどではなくデータを保存していることを示している。

引用元:IT用語辞典e-Words:DATファイル 【 DAT file 】 .datファイル

ちなみに、通常アプリケーションでdatファイルを開くことが出来ないと思います。だからと言って拡張子を変更する場合はデータの破損に繋がる場合がありますので、ご注意下さい。

ファイル名を編集して.datの拡張子の直接変更してしまえば、その拡張子に対応するアプリケーションで簡単に開くことができるが、ファイルタイプを誤って指定してしまうと、ファイルを開こうとしたときにデータの破損を招くおそれがある。

拡張子辞典BINARY:.dat

エラーチェックと4つの関数 #04-05

#04 エラーチェックをしていこう

入力値のエラーチェックは次の2段構えとなっております。

  1. message, userに入力値がある場合に処理を始める
  2. メッセージが空文字列じゃない時に書き込み処理

1はissetでPOSTがNULLかどうかを判定していて、2はメッセージが空文字列「""」ではないかを判定する。何故2つのチェックが必要なのか?その理由を確かめるために実験してみました。

messageとuserの値、$POST['message']と$POST['user']をvar_dump()で確認してみることに。

 

サイトを開いた時
$_POST 中身
message NULL
user NULL

 

未入力で送信
$_POST 中身
message ""
user ""

実験の結果、1は初めてサイトを訪れた時、2は送信を押された以降である時を判断していることがわかりました。今回のソースコードだと、1があるおかげで、サイトを訪れた時は何もしないということになります。これで、処理が速まるわけですね。

ちなみに、nullとfalseと空文字と0についてですが、これらを「==」で比較した場合はすべてtrueということで、どれも同じ扱いになります。だけど、「===」で型まで比較した場合は、falseとなります。

以下に詳しい記載があります。

PHPでnullとfalseと空文字と0は同じ?

4つの関数

以下4つの関数について把握しておきましょう。

  1. isset(チェックする変数)
  2. trim(文字列, 取り除くもの)
  3. date(フォーマット, タイムスタンプ)
  4. str_replace(検索する値, 置き換える値, 対象)

isset(チェックする変数)

変数がNULLかどうかを調べます。NULLではないなら、true。NULLだとfalseを返します。

ちなみに、unset()はtrue、falseが逆になったものではなく、指定した変数を破棄するという全く別モノですのでご注意を。また、配列用にarray_key_existsというのもあります。

trim(文字列, 取り除くもの)

文字列の先頭と末尾にあるスペースを取り除きます。第2引数を省略した場合は以下の文字を削除します。

  • " " (ASCII 32 (0x20)), 通常の空白。
  • "\t" (ASCII 9 (0x09)), タブ。
  • "\n" (ASCII 10 (0x0A)), リターン。
  • "\r" (ASCII 13 (0x0D)), 改行。
  • "\0" (ASCII 0 (0x00)), NULバイト
  • "\x0B" (ASCII 11 (0x0B)), 垂直タブ

引用元:PHPマニュアル:trim

試しにtrimで実験してみました。以下のサンプルでより理解が深まると思います。

<?php
// 文字化け対策
header('Content-Type: text/html; charset="UTF-8"');

// 半角スペース   
$space1 = "  aabb  "; // 開始と終わりに2文字
$space2 = "aa     bb"; // 真ん中に5文字
$space3 = "  aa     bb  "; // space1, 2の組み合わせ

// タブ
$tab1 = "\t\taabb\t\t"; // 開始と終わりに2文字
$tab2 = "aa\tbb"; // 真ん中に1文字
$tab3 = "\t\taa\tbb\t\t"; // space1, 2の組み合わせ

// 半角スペースの除去
$after_space1 = trim($space1);
$after_space2 = trim($space2);
$after_space3 = trim($space3);

// タブの除去
$after_tab1 = trim($tab1);
$after_tab2 = trim($tab2);
$after_tab3 = trim($tab3);
?>

実際にブラウザで表示した結果は次の通りです。

space1, 2, 3の表示
$space1     string(8) "  aabb  "
$after_space1   string(4) "aabb"

$space2     string(9) "aa     bb"
$after_space2   string(9) "aa     bb"

$space3     string(13) "  aa     bb  "
$after_space3   string(9) "aa     bb"

tab1, 2, 3の表示
$tab1       string(8) "     aabb        "
$after_tab1 string(4) "aabb"

$tab2       string(5) "aa   bb"
$after_tab2 string(5) "aa   bb"

$tab3       string(9) "     aa  bb      "
$after_tab3 string(5) "aa   bb"

文字列の先頭と末尾にあるスペースがすべて取り除かれましたね。文字の途中にあるものは取り除かれません。

念のためこの結果表示のソースコードも貼っておきます。

<?
// 続き

// 変数の表示
echo "<pre>";
// space1, 2, 3の表示
echo "【space1, 2, 3の表示】\n";
echo "\$space1\t\t";
var_dump($space1);
echo "\$after_space1\t";
var_dump($after_space1);
echo "\n";

echo "\$space2\t\t";
var_dump($space2);
echo "\$after_space2\t";
var_dump($after_space2);
echo "\n";

echo "\$space3\t\t";
var_dump($space3);
echo "\$after_space3\t";
var_dump($after_space3);
echo "\n";

// tab1, 2, 3の表示
echo "【tab1, 2, 3の表示】\n";

echo "\$tab1\t\t";
var_dump($tab1);
echo "\$after_tab1\t";
var_dump($after_tab1);
echo "\n";

echo "\$tab2\t\t";
var_dump($tab2);
echo "\$after_tab2\t";
var_dump($after_tab2);
echo "\n";

echo "\$tab3\t\t";
var_dump($tab3);
echo "\$after_tab3\t";
var_dump($after_tab3);
echo "</pre>";
?>

#05 投稿時刻も保存してみよう

date(フォーマット, タイムスタンプ)

タイムスタンプを省略した場合は、現在の時刻のタイムスタンプが使用されます。以下サンプルです。実験してみました。

実験時の日付は2013/10/14(月)です。

<?php
// タイムゾーンを東京に設定
date_default_timezone_set('Asia/Tokyo');

$date = array(); // 配列の宣言

$date['Y-m-d H:i:s'] = date('Y-m-d H:i:s');
$date['Y-m-d\\tH:i:s'] = date('Y-m-d\tH:i:s');
$date['y-M-D h:i:s'] = date('y-M-D h:i:s');
$date['Y-m-d'] = date('Y-m-d');
$date['w'] = date('w'); // 0 (日曜)から 6 (土曜)
$date['W'] = date('W'); // ISO-8601 月曜日に始まる年単位の週番号

echo "<pre>";
print_r($date);
echo "</pre>";
?>

結果

Array
(
    [Y-m-d H:i:s] => 2013-10-14 09:32:26
    [Y-m-d\tH:i:s] => 2013-10-14t09:32:26
    [y-M-D h:i:s] => 13-Oct-Mon 09:32:26
    [Y-m-d] => 2013-10-14
    [w] => 1
    [W] => 42
)

2つ目にはタブ文字が書かれて言いますが、「\」が取り除かれて「t」だけが表示されています。これはdateの引数がシングルクォーテーションで囲まれているからです。ダブルクオーテーションで囲めばタブ文字に展開されます。

$date['Y-m-d\\tH:i:s'] = date("Y-m-d\tH:i:s");

// 結果
Array
(
    [Y-m-d\tH:i:s] => 2013-10-14    09:35:36
)

str_replace(検索する値, 置き換える値, 対象)

$message = str_replace("\t", ' ', $message);

今回はタブを半角スペースに変換しています。

str_replaceは置換用の関数ですが、削除のために使うことも出来ます。trimと違って、先頭と末尾だけではなく文字列全体から取り除くことが出来ます。

<?php
// 文字列の用意
$str = "  ab cc    ttt    ";
$after_str = str_replace(" ", "", $str);

// 文字列の表示
echo "<pre>";
var_dump($str);
print "\n";
var_dump($after_str);
echo "</pre>";
?>

結果

string(18) "  ab cc    ttt    "

string(7) "abccttt"

このように、検索する値を半角スペースに、置き換える値を空文字にすると、指定した値をすべて取り除くことが出来ます。さらに、応用で複数の値を取り除くことも出来ます。

<?php
// 文字列の用意
$str = "  ab cc\t\t kk\t  ttt    ";

$delete = array(" ", "\t");
$after_str = str_replace($delete, "", $str);

取り除くものを配列で用意しておけば、1つの引数として扱えます。

以下、結果(ソースはさきほどと同じ)

string(22) "  ab cc      kk   ttt    "

string(9) "abcckkttt"

datファイルからデータ読み込む #06

#06 投稿データを読み込もう

エスケープ

エスケープ処理についてですが、htmlspecialcharsを使わないと入力された値がプログラムとかだと、それが実行されてしまう時があります。例えば、のようにjavascriptでalertという関数が実行されたりすると、ポップアップといって、いきなり小さなウィンドウが出てきたりします。そういういたずらを防ぐために、htmlspecialcharsを使うわけです。

転載元:【ドットインストール】PHPカレンダーの解説

ここからは以下の関数について説明

  1. file(読み込むファイル, オプション)
  2. array_reverse(配列, オプション)

file(読み込むファイル, オプション)

ファイル全体を読み込んで配列に格納します。ファイルの内容を文字列として返すには file_get_contents() 使用して下さい。

オプションについては以下のとおりです。

定数 意味
FILE_IGNORE_NEW_LINES 配列の各要素の最後に改行文字を追加しません。
FILE_SKIP_EMPTY_LINES 空行を読み飛ばします。

つまり、今回のコード

$posts = file($dataFile, FILE_IGNORE_NEW_LINE);

これは$dataFileの各行を配列に加えていきますが、改行文字を追加しないということです。テキストには見えなくても、改行したら\nが末尾にありますので、それを無視します。

array_reverse(配列, オプション)

指定した配列の要素の順番を逆にします。オプションはtrueでインデックス(キー)を保持させることができます。つまり、逆転した後も、インデックスと要素の組み合わせは同じままになるということです。

以下サンプル

<?php
// 文字化け対策
header('Content-Type: text/html; charset="UTF-8"');

$input  = array("0番", "1番", array("2の1", "2の2"));
$reversed = array_reverse($input);
$preserved = array_reverse($input, true);

echo "<pre>";
echo "---元の配列---\n";
print_r($input);

echo "\n---逆転後---\n";
print_r($reversed);

echo  "\n---オプションあり(インデックスを保持する)---\n";
print_r($preserved);
echo "</pre>";
?>

結果

---元の配列---
Array
(
    [0] => 0番
    [1] => 1番
    [2] => Array
        (
            [0] => 2の1
            [1] => 2の2
        )

)

---逆転後---
Array
(
    [0] => Array
        (
            [0] => 2の1
            [1] => 2の2
        )

    [1] => 1番
    [2] => 0番
)

---オプションあり(インデックスを保持する)---
Array
(
    [2] => Array
        (
            [0] => 2の1
            [1] => 2の2
        )

    [1] => 1番
    [0] => 0番
)

結果を見てみるとわかりますが、逆転するのは1次元までです。ネストされた配列までは逆転されません。すべて逆転したい場合は以下のようにします。

わかりやすくため、オプションをtrueでインデックスを保持しています。

<?php
// 文字化け対策
header('Content-Type: text/html; charset="UTF-8"');

$input  = array("0番", "1番", array("2の1", "2の2"));

// $inputの中身を書き換えるため、先に表示
echo "<pre>";
echo "\n---逆転前---\n";
print_r($input);
echo "</pre>";

// 配列の中の配列を逆転
foreach ($input as $key => $value) {
    // ネストされた要素が1つの時にarray_reverseを使うとエラーになります
    // 1次元配列の時は、要素が1つでもエラーは起きません。
    if (count($input[$key]) >= 2) {
        $input[$key] = array_reverse($input[$key], true);
    }
}

// 配列を逆転
$reversed = array_reverse($input, true);

// 結果を表示
echo "<pre>";
echo "\n---逆転後---\n";
print_r($reversed);
echo "</pre>";
?>

結果

---逆転前---
Array
(
    [0] => 0番
    [1] => 1番
    [2] => Array
        (
            [0] => 2の1
            [1] => 2の2
        )

)
---逆転後---
Array
(
    [2] => Array
        (
            [1] => 2の2
            [0] => 2の1
        )

    [1] => 1番
    [0] => 0番
)

このコードは2次元配列までしか対応していませんのでご注意を。

datファイルの中身を表示 #07

#07 投稿データを表示してみよう

まずは2つの関数を把握しましょう。

  1. list(置き換える変数)
  2. explode(区切り文字列, 入力文字列)

list(置き換える変数)

list()はarray() と同様に、 実際には関数ではなく言語構造です。list()に配列を代入すると、各要素がlist()で指定した変数に格納されます。

<?php
// 文字化け対策
header('Content-Type: text/html; charset="UTF-8"');

$array = array('1個目', '2個目', '3個目',array("4の1", "4の2"));

// 配列をリストに変換
list($n1, , $n3, list($n4_1, $n4_2)) = $array; // 部分的に変換もできます。
echo    "\$n1 = {$n1}<br>".
        "\$n2は存在しない<br>".
        "\$n3 = {$n3}<br>".
        "\$n4_1 = {$n4_1}\t\$n4_2 = {$n4_2}<br>";

// list() は文字列では動作しません
list($str) = "text";
var_dump($str); // NULL
?>

結果

$n1 = 1個目
$n2は存在しない
$n3 = 3個目
$n4_1 = 4の1   $n4_2 = 4の2
NULL

list()はPDOでデータベースからレコードセットを取得するときに便利です。各カラムがlistに変換されます。

while (list($id, $name, $salary) = $result->fetch(PDO::FETCH_NUM)) {}

explode(区切り文字列, 入力文字列)

文字列を文字列により分割します。そして、配列として返します。

<?php
// 文字化け対策
header('Content-Type: text/html; charset="UTF-8"');

$num  = "num1,num2,num3";
$nums = explode(",", $num);
echo $nums[0]; // num1
echo $nums[2]; // num3
?>

結果

num1num3

以上2つの関数を組み合わせて、今回コメントを展開しています。

<?php list($message, $user, $postedAt) = explode("\t", $post); ?>

これは1行の文字をタブで区切り、それぞれを変数に代入しています。

図:datファイルを配列にする

CSRFとは何か? #08-09

#08 CSRF対策を施そう (1)

#09 CSRF対策を施そう (2)

CSRF【 Cross Site Request Forgeries 】とは攻撃手法の一つです。仕掛けられたリンクを踏むと気づかない内に自分がプログラムを実行させられているというものです。「自分が」実行しているという扱いがポイントです。

この攻撃手法を使った事件が次の記事で紹介されています。

クリックだけで勝手に書き込み…「CSRF脆弱性」とは?

リンクをクリックしただけで、知らないうちに自分が犯行声明を書き込んでいたという事件です。

この事件の流れを理解するため、例えの図を作りました。

図:CSRFの例

このようにリンクの裏で勝手に処理が行われているのがCSRFです。ユーザーがソースを確認すればこれが仕込まれているのがわかるらしいのですが、毎回リンクを押す度に確認なんかしませんよね。なので、上記の記事にも書かれていますが、怪しいリンクはクリックしないのが1番の対策です。

余談ですが、紹介した事件では大学生が誤認逮捕されたみたいですが、CSRFによって書き込まれた殺人予告が2秒で書き込まれました。しかし、内容は250文字もあり、タイピングで2秒で書き込むのは難しいのです。いや、コピペでも難しいでしょう。

警察はこの疑問点を放置していたと記事に書いてありました。プログラムは量産できるので、いちいち調べてたキリがないかもしれません。なので、調べる側も小さな事件はすべて洗い出すのは酷でしょう。そこで、予めこの問題の対策をしておきます。それがCSRF対策です。

さて、CSRFがどのようなものかわかったところで、その対策をみていきましょう。今回は、細かいとは省いて流れを理解することにします。

ソースコードのロジックは以下の通りです。

  1. サイトを訪れた時にトークンを作成
    1. 乱数からsha1()でハッシュ値(証明書)を作成
    2. セッションに1をトークンとして保存(サーバーに保存)
  2. formを送信した時にトークンをチェックする
    1. トークンが空、またformに仕込んだトークンが改ざんされていた。
    2. 不正な送信が行われたことを通告する

次はソースコードを見て確認してみましょう。ハイライトされた行がCSRF対策に関わる処理です。見やすくするため、それ以外の処理のところはコメントで処理内容だけ記載して省略しています。

<?php

$dataFile = 'bbs.dat';

// CSRF対策
session_start(); // セッションを使う前に実行しておく

function setToken() {
    $token = sha1(uniqid(mt_rand(), true));
    $_SESSION['token'] = $token;
}

function checkToken() {
    if (empty($_SESSION['token']) || ($_SESSION['token'] != $_POST['token'])) {
        echo "不正なPOSTが行われました!";
        exit;
    }
}

// エスケープ関数hの定義

if ($_SERVER['REQUEST_METHOD'] == 'POST' && 
    isset($_POST['message']) &&
    isset($_POST['user'])) {

    checkToken();

   // コメントと投稿者のスペースを取り除く

    if ($message !== '') {
        // コメントをdatファイルに保存
    }
} else {
    setToken();
}

// datファイルのコメントを配列に変換

?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <!-- 略 -->
</head>
<body>
    <h1>簡易掲示板</h1>
    <form action="" method="post">
        <!-- 入力エリア -->
        <!-- 送信ボタン -->
        <input type="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>">
    </form>
    <!--投稿一覧 -->
</body>
</html>

さて、全体を俯瞰したところでsha1()で作るハッシュ値とトークンについて触れていきます。
今回、サイトを訪れた時に乱数を発生させています。そして、sha1でその乱数からハッシュ値というものを作成しています。このハッシュ値ですが、これは証明書です。サイトを訪れた時に作られた乱数を証明しています。同じ数値からしか同じハッシュ値は作成できません。

図:ハッシュの説明

なので最初に作られた乱数のハッシュ値を証明書としてセッションに保存しておけば、証明書が発行されたこのサイトからのデータの送信かがわかります。今回はこの証明書をトークン、$tokenで管理しています。

図:トークンをセッションに保存

このトークンがformのhiddenフィールドにセットしてあるので、このサイトからform送信をしないと正常なトークンが送信されません。他のサイトからリクエストを受け付けたとしても、POST['token']が空であったり、違う値が入っていたりします。そもそも、セッションでトークンを保持していないといけないので、チェックするトークン自体を持ち合わせていないと場合もあります。

以上がCSRF対策について解説です。このレッスンをきっかけに自分で調べてまとめてみました。稚拙な備忘録なので、理解できないかもしれません。

おまけ クッキーとセッションの違い

この2つの違いは保存先です。クッキーだとクライアント側、ブラウザにデータが保存してあるので、改ざんされてしまう可能性があります。なので、セッションにしておけば、サーバーに保存されるので改ざんの心配がありません。

保存方法 保存先
クッキー ブラウザ
セッション サーバー

サーバー自体が攻撃を受けたらわかりません。