Kintarou'sBlog

プログラミング学習中。学習内容のアウトプットや読書で学んだことなど随時投稿!

【PHP】トークン機能を実装する

こんにちは😊Kintarouです。

現在エンジニア転職を目指してプログラミング学習中です👨‍🎓
夢はフリーランスエンジニアになって働く人にとって働く事が楽しくなるシステムを作ること!
と、愛する妻と海外移住すること🗽

プログラミングや読んでいる本のことなど、ブログに書いていきます!
twitter : https://twitter.com/ryosuke_angry


今回参考にさせて頂いたサイト様🙇‍♂️ dotinstall.com


CSRFの一例

まずはCSRFにてどのように攻撃されるかの一例を見ていきます。
前回記事でも用いた投稿機能のあるPHPファイルを使います。

ryosuke-toyama.hatenablog.com

■messages.txt

hello
hi

■index.php

<?php

require('../app/functions.php');

define('FILENAME', '../app/messages.txt');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  $message = trim(filter_input(INPUT_POST, 'message'));
  $message = $message !== '' ? $message : '...';
  
  $fp = fopen(FILENAME, 'a');
  fwrite($fp, $message . "\n");
  fclose($fp);

  header('Location: http://localhost:8080/result.php');
  exit;
}

$messages = file(FILENAME, FILE_IGNORE_NEW_LINES);

include('../app/_parts/_header.php');

?>

  <ul>
  <?php foreach ($messages as $message): ?>
    <li><?= h($message); ?></li>
  <?php endforeach; ?>
  </ul>
  <form action="" method="post">
    <input type="text" name="message">
    <button>Post</button>
  </form>

■result.php

<?php

require('../app/functions.php');

$message = trim(filter_input(INPUT_POST, 'message'));
$message = $message !== '' ? $message : '...';

$filename = '../app/messages.txt';
$fp = fopen($filename, 'a');
fwrite($fp, $message . "\n");
fclose($fp);

include('../app/_parts/_header.php');

?>

  <p>Message added!</p>
  <p><a href="index.php">Go back</a></p>

f:id:ryosuke-toyama:20201105205217p:plain
f:id:ryosuke-toyama:20201105205241p:plain
f:id:ryosuke-toyama:20201105205303p:plain

ここにもし、以下のようなサイトがあった場合の挙動を見てみます。

f:id:ryosuke-toyama:20201105214825p:plain

仮にここで『今すぐ受け取る!』ボタンを押すと下記のような投稿がされてしまいます。

f:id:ryosuke-toyama:20201105205241p:plain
f:id:ryosuke-toyama:20201105222333p:plain

何故このような投稿がされてしまったのかを先ほどの怪しげなサイトのソースから確認してみます。

■winner.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>当選しました!</title>
</head>
<body>
  <h1>おめでとうございます!</h1>

  <!-- 指定したURLにPOSTで情報を送るフォームになっています。 -->
  <form action="http://localhost:8080/index.php" method="post">
    <p>100万円が当たりました!下のボタンをクリックしてください!</p>

    <!-- hiddenとなって隠されていますが、messageという名前で犯行予告!!!!というデータが送られるようになっています。 -->
    <input type="hidden" name="message" value="犯行予告!!!!">
    <button>今すぐ受け取る!</button>
  </form>
</body>
</html>

このように、ブラウザからページのソースで必要な情報がわかってしまうので別のサイトからでもデータを送る事ができ、反映されてしまします。

対策としては各種フレームワークを使うか、トークンを生成してそれが一致しているかを調べる必要があります。

今回はトークンを生成し、一致していた場合に処理を行う機能を実装していきます。

トークンの生成とチェック機能を実装する

トークン生成とチェック機能をfunctions.phpに記述していきます。

■functions.php

<?php

function h($str)
{
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}


function createToken() {

  #$_SESSION['token']がセットされていない場合にトークンを生成します。
  if (!isset($_SESSION['token'])) {

    #random_bytes()関数で32文字のランダムな文字列を生成します。
    #random_bytes()で生成されるのはバイナリ(2進数)なのでbin2hex()関数で16進数に変換します。
    $_SESSION['token'] = bin2hex(random_bytes(32));
  }
}

function validateToken() {
  if (

    #$_SESSION['token']が空、もしくはPOSTで送られたtokenとは違う場合exit()で処理を終了させます。
    empty($_SESSION['token']) ||
    $_SESSION['token'] !== filter_input(INPUT_POST, 'token')
  ) {
    exit('Invalid post request');
  }
}

#$_SESSION['token']でsessionを使うので、ここでsession_start()しておきます。
session_start();

トークン生成とチェック機能を組み込む

index.phpに組み込んでいきます。

■index.php

<?php

#functions.phpを読み込みます。
require('../app/functions.php');

#早速トークンを生成します。
createToken();

define('FILENAME', '../app/messages.txt');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

  #POSTでデータが送信された際、下部フォームによって送られたトークンをチェックします。
  #これでもしトークンが無い、トークンが誤っている(外部サイトからのPOST送信である)場合は処理が終了する。
  validateToken();
  
  $message = trim(filter_input(INPUT_POST, 'message'));
  $message = $message !== '' ? $message : '...';
  
  $fp = fopen(FILENAME, 'a');
  fwrite($fp, $message . "\n");
  fclose($fp);

  header('Location: http://localhost:8080/result.php');
  exit;
}

$messages = file(FILENAME, FILE_IGNORE_NEW_LINES);

include('../app/_parts/_header.php');

?>

  <ul>
  <?php foreach ($messages as $message): ?>
    <li><?= h($message); ?></li>
  <?php endforeach; ?>
  </ul>

  <form action="" method="post">
    <input type="text" name="message">
    <button>Post</button>

    <!-- このファイルの冒頭で生成したトークンをパラメータとして一緒に送ります。 -->
    <input type="hidden" name="token" value="<?= h($_SESSION['token']); ?>">
  </form>

上記で更新したページのソースを確認すると、ランダムなトークンが生成されており、用意に攻撃する事が出来なくなっている事がわかります。
f:id:ryosuke-toyama:20201105222804p:plain

補足

トークンを実装することで今回のようなCSRFは防ぐ事が出来ますが、攻撃方法は多種多様に変化していくので、やはりフレームワークを使った開発が賢明です。

以上、どなたかの参考になれば幸いです😊