WebEngine

だらだらと綴る技術系メモ

PHPでクロスサイトリクエストフォージェリ対策を実装

クロスサイトリクエストフォージェリCSRF)とは?

ユーザの意図しない操作を、そのユーザの権限で実行させてしまうという恐ろしい攻撃です。CSRFと略称で呼ばれること が多いです。
たとえば、情報発信系サービスであれば、AさんがログインすればAさんしかメッセージを発信できないはずです。しかし、 CSRFの被害にあうと、攻撃者のメッセージがAさんが発信したものとして処理されてしまいます。
ほかにどんな被害を受けるかというと、

  • 意図しない商品購入
  • 意図しない退会処理
  • 意図しない投稿

などが代表的です。

PHPでの対策

ここでは、一般的な対策法をPHPで実装してみます。

<?php

@session_start();

/* POST以外でアクセスされたとき(普通にページに入ってきたときも含まれる) */
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
    setToken();
    checkDump();
/* POSTでアクセスされたとき */
} else {
    checkToken();
    checkDump();
}

/* トークンをセッションにセット */
function setToken() {
    $token = rtrim(base64_encode(openssl_random_pseudo_bytes(32)),'=');
    $_SESSION['token'] = $token;
}

/* トークンをセッションから取得 */
function checkToken() {
    if(empty($_SESSION['token']) || ($_SESSION['token'] != $_POST['token'])) {
        echo 'CSRFの可能性アリ';
    }
}

/* チェック用関数 */
function checkDump() {
    if (isset($_POST['token'])) {
        var_dump($_SESSION['token']);
        var_dump($_POST['token']);
    }
}

?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>PHPでCSRF対策</title>
  <meta charset="utf-8">
</head>
<body>
  <form method="post" action="">
    <input type="hidden" name="token" value="<?=htmlspecialchars($_SESSION['token'], ENT_QUOTES, 'UTF-8')?>">
    <input type="submit" value="送信">
  </form>
</body>
</html>

setToken関数でランダムな文字列を生成し、それをhiddenで送信します。その文字列をサーバサイド で管理し、比較するといった流れです。文字列はセッションで管理します。
トークンの生成を

$token = sha1(uniqid(mt_rand(), true));

としているサイトが多いですが、これは理解を容易にするためです。トークン生成には、 暗号論的擬似乱数生成器によるもののほうが安全であるという意見があり、今回はあえて そちらで実装をしてみました。mcrypt_create_ivを利用しても良いですね。
なお、openssl_random_pseudo_bytesはPHP5.3.0以降でないと使えない のであしからず。

登録ボタンを押したところ、僕の場合、実行結果(ダンプ処理の結果)は

string 'j7C6ES2rXWczWtM/2mHP3EL/YIJcGbQsuv6jPMEq7xE' (length=43)
string 'j7C6ES2rXWczWtM/2mHP3EL/YIJcGbQsuv6jPMEq7xE' (length=43)

と表示されました。ちゃんと正規のリクエストなので文字列が一致しているのが確認できます。

不正リクエストを送ってみる

念のため、GETでアクセスされたときどうなるのかも見てみましょう。フォームの送信手法を

<form method="get" action="">

と変更します。それからチェック用につくった関数であるcheckDump関数のPOSTを

function checkDump() {
    if (isset($_GET['token'])) {
        var_dump($_SESSION['token']);
        var_dump($_GET['token']);
    }
}

のようにGETに変更します。この関数は単なるチェック用なのでCSRF対策用の関数に 影響を与えることはありません。

僕の場合は

string 'PcCRYNLU9U2hNV8hlc36C27KitYAGJ9+Kkp28d7+cZo' (length=43)
string 'EwoPwaGwbhdQyyUQxLI1jcDLf36EN6IaDg023xmd+uk' (length=43)

と表示されました。ちゃんと異なる文字列になっていることがわかります。

参考

PHP簡易カレンダー作成

DateTimeクラスで簡易カレンダーを実装

date()関数で書いても良いのですが、サンプルがインターネット上に結構あったので、今回はDateTimeクラスで実装してみようと思います。DateTimeクラスは日付をオブジェクトとして扱うことのできるクラスです。個人的には、date()関数よりすっきり書ける印象で、こちらの方が好きです。

※ DateTimeクラスはPHP 5.2.0以降から利用可能です。

<?php

$weekday = array('','','','','','','');
$day = 1;

$datetime = new DateTime("Asia/Tokyo");

/* 現在の年、月を取得 */
$year = $datetime->format('Y');
$month = $datetime->format('m');

/* 現在の月の1日目の曜日を割り出すため現在の日付を1日目として設定する */
$datetime->setDate($year, $month, 1);
$firstWeekDay = (int)$datetime->format('w');
$lastDay = (int)$datetime->format('t');

?>
<!DOCTYPE html>
<html lang="ja">
<meta charset="utf-8">
<head>
<title>PHPカレンダ</title>
</head>
<body>

<table>
<figcaption><?=$year.''.$month.''?></figcaption>
<tr>
  <?php foreach($weekday as $value): ?>
    <th><?=$value?></th>
  <?php endforeach; ?>
</tr>
<?php
/*
  1日目がはじまるのが日曜じゃないなら、
  はじまるまでの数日分を空白で埋める
*/
 for ($i=0; $i<$firstWeekDay; $i++) {
  echo '<td> </td>';
}

/* 6(週)*7(曜日)=42のマスでカレンダを構成 */
for ($cell=$firstWeekDay; $cell<42; $cell++) {

  /* 日曜日に改行させたいから */
  if ($cell%7 === 0) {
    echo '</tr><tr>';
  }

  /* 日付を出力、最終日を過ぎれば空白で埋める */
  if ($day <= $lastDay) {
    echo '<td>'.$day.'</td>';
  } else {
    echo '<td> </td>';
  }

  $day++;
}
?>
</tr>
</table>

</body>
</html>

あんまり綺麗なコードじゃありませんね・・・。将来再チャレンジして、どのくらい成長したか計ってみるのも良いかもしれません。

タイムゾーンの確認

DateTimeクラスをインスタンス化する際、引数にタイムゾーンを設定していますが、この方法は、あまり見かけません。(あんまり良くないのかもしれない)
通常は

<?php
$datetime->setTimezone(new DateTimeZone('Asia/Tokyo'));

みたいに書きます。

下記のコードで時間帯がどこに設定されているかチェック可能です。

<?php
$timeZone = $datetime->getTimezone();
echo $timeZone->getName();

ダンプしても確認できました。

<?php
var_dump($datetime);

参考

http://qiita.com/re-24/items/c3ed814f2e1ee0f8e811

gitを使ってみよう(実践編)

gitの流れ

前回、testというディレクトリを作業ディレクトリ として初期化しました。今回は、このディレクトリ内でgitコマンド を使っていきたいと思います。
とりあえずtest.txtなどのなにかしらのファイルをつくってみましょう。

Hello,Git!!

作成できたら、以下のようなコマンドを打ちます。

git add test.txt

addコマンドは、指定ファイルをステージング エリアとかインデックスなどと呼ばれる場所に登録します。
ちなみに add.で、そのディレクトリ下に存在するすべてのファイルを指定できます。

つづいて、以下のようなコマンドを入力。

git commit -m"First Commit!"

commitコマンドは、ステージングエリア内に存在するファイルをリポジトリ という場所に上げます。commitされたファイルは履歴として残ります。
git statusコマンドで現在の状態を確認し、

# On branch master
nothing to commit (working directory clean)

というような表示がされればコミットに成功しています。
git logでも良いです。 -mはコメントを記録するオプションです。

今までの一連の流れを絵でまとめると、こんなイメージです。
f:id:web-engine:20160528180947p:plain


差分を確認する

test.txtのHello,Git!!を改行して、 Hello,World!!とでも入力してみましょう。
gitでは、以下のコマンドで、指定ファイルの変更箇所を 表示してくれます。ただし、まだステージングエリアにaddしていないファイル に限ります。

git diff test.txt

追加された部分の前には+がつきます。

ステージングエリアで差分を確認したい場合には

git diff --cached

と入力しましょう。このオプションはコミット前の確認に使うことが多いです。


前回コミットした状態へ戻す

前回コミットした状態へ戻したい場合、以下のコマンドを打てば実現できます。

git reset --hard HEAD

HEADが最後のコミットの位置を表しています。
さらに一つ前のコミットに戻したい場合は

git reset --hard HEAD^

が使えます。


チームで使う場合

ここまで紹介したのは、1人でgitを使う場合の基本コマンドです。
複数人でgitを利用する場合、pushpull などのコマンドを覚える必要があります。かの有名なドットインストールで git入門をやっているので、そちらを参照するのが早いでしょう。


もっと詳しく知りたい方

http://qiita.com/opengl-8080/items/451c5967cbbc262f4f0d

こちらも参考に