より高度なCSRF対策 – URL/URI個別にバリデーションする方法でハッシュ(HMAC)を使えばデータベースを使わずに有効期限付きのURLを作れる、と紹介しました。今回はパスワード付きURL(URI)の作り方を紹介します。
まず、安全性についてはハッシュ(HMAC)を使って有効期限付きURLを作る方法を参照してください。
ハッシュ(HMAC)を使ってパスワード付きURL/URIを作る方法
ハッシュ(HMAC)を使って有効期限付きURL/URIを作る方法とほとんど同じです。以下の方法でデータベースを使わずにパスワード付き(この例の場合は有効期限付き)のURIを作れます。
<?php
// 有効期限付きURIの作成
// 有効期限付きURIにするURI
$uri = 'http://example.com/some_path';
// 秘密鍵 - 'secure random data'はrandom_bytes()などで生成した安全な値
define('SUPER_SECRET_MASTER_KEY', 'secure random data');
$salt = bin2hex(randome_bytes(32)); // 事前共有鍵
$expire = time() + 3600; // 有効期限
$context = serialize([$uri, $expire]); // コンテクスト - $uriと$expire
$prk = hash_hmac('sha256', SUPER_SECRET_MASTER_KEY, $salt);
$derived_key = hash_hmac('sha256', $prk, $context); // 導出鍵(パスワード) - これをユーザーに渡す
$params = http_build_query(['key2' => $salt, 'context' => $context]);
// 要パスワード+有効期限付きURI - これをユーザーに渡す
$uri_with_expiration = $uri . '?' . $params);
?>
<?php
// 要パスワード+有効期限付きURIのチェック
// 導出鍵(パスワード)が無い場合、導出鍵(パスワード)を入力する
// フォームなどを開いてユーザーに送信させる。
// ユーザーから送られてきた導出鍵(パスワード)
$derived_key_entered = $_GET['key1'];
// 送られてきた事前共有鍵 - ユーザーに渡したURL
$salt_from_request = $_GET['key2'];
// コンテクスト - $uriとexpire
$uri_requested = get_uri_without_keys($_SERVER['REQUEST_URI']); // この関数でkey2とexpireを削除
$context = serialize([$uri_requested, $_GET['expire']);
$prk = hash_hmac('sha256', SUPER_SECRET_MASTER_KEY, $salt_from_request);
$derived_key = hash_hmac('sha256', $prk, $context);
// 暗号学的に安全な導出鍵が一致すればアクセスOK
if (!hash_equals($derived_key_entered, $derived_key)) {
throw new Exception('不正なリクエスト');
}
if ($_GET['expire'] < time()) {
throw new Exception('有効期限切れ');
}
// アクセスOK
?>
まとめ
有効期限付きURL/URIと異る部分は
- HMACによって導出された鍵をURLに付けず、ユーザーに入力させる
だけです。
確実に正しい(到達可能な)メールアドレスを入力したユーザーにだけ参照/ダウンロードを許可する、といった場合に便利です。
- ページにアクセスした際に「このページの参照にはメールアドレスの確認が必要です。メールアドレスを入力してください」といったフォームを表示する。
- ユーザーがメールアドレスフォームを使ってパスワードをリクエストする。
- サーバーは生成した鍵をメールを使ってユーザーに通知すると同時に「メールで送信したパスワードを入力してください」といったフォームを表示する。
- ユーザーがパスワードを送信し、サーバーはパスワードが正しければアクセスを許可する。
例えばセッションと組み合わせて使えば1、そのセッションだけ参照/ダウンロード可能とすることも簡単です。ユーザーID/グループIDでも同様なことが可能です。
ユーザーに入力させるパスワード(導出鍵)は最初の10文字程度でも十分な場合が多いでしょう。この場合、HMACハッシュ値の必要文字数分だけ同じかチェックします。
- セッション限りとするにはsalt(事前共有鍵)を$_SESSIONに保存し、saltが一致しているか確認するだけです。 ↩