Laravel環境でSlackのリクエスト署名検証を実装する

Slack
two 3d humans give their hand for handshake
この記事は約7分で読めます。
スポンサーリンク

過去に何度かSlackBotの解説を行ってきましたが、今回はリクエスト署名検証について解説していきます。

今回の記事は

・Slackでセキュリティ対策をしないとどうなるのか
・リクエスト署名検証がなぜ推奨されているのか
・どのように実装をすればいいのか

についてわかる記事となっています。

Slackのセキュリティ対策

セキュリティ対策をしないとどうなるのか

Slackからのリクエストを受け取るためには、Slackからのアクセスを許可する環境でなければいけませんが、SlackはIPアドレスが固定されていません。

そのためどのIPアドレスからでもアクセス出来るような状態に晒されることとなります。

そのためSlackがデータを送信するURLさえ知っていればパラメータを偽装してそのURLに送信することで都合よくデータを書き換えることが出来てしまいます。

Slackのために作ったAPIに対して、Slack以外の場所から実行される危険性を防ぐためにセキュリティ対策は絶対に必要です!

検証トークン(Verification Token)の検証ではダメなのか

Slackの検証には2つの方法があります。

1つは今回紹介するリクエスト署名を用いた検証で、もう1つは検証トークンによる検証方法です。

実は過去に紹介したIntegromatを使ったSlackアプリ開発の際は検証トークンの方法で検証を行っています。

検証トークンを用いた方法でも悪意のあるユーザーからのアクセスを防ぐことは出来ますが、検証トークンが万が一バレてしまった場合、セキュリティがない状態と同様に自由にアクセスされてしまいます。

リクエスト署名を用いた検証方法が出来ない環境の場合はこちらでも良いのですが、可能であれば次に紹介するリクエスト署名を用いた方法を使用してください。

Slackのリクエスト署名検証がなぜ安全なのか

Slackのリクエスト署名検証はSlackのサーバーでシークレットキー、タイムスタンプ、投稿内容から解読できない暗号文(ハッシュ)を生成して開発サーバーに贈ります。

開発サーバーでもSlackと同様にシークレットキー、タイムスタンプ、投稿内容からハッシュを作り、そのハッシュが一致しているかでチェックを行います。

万が一、検証トークンと同じようにハッシュを見られてしまったとしても、ハッシュから元の文章に戻すことは出来ないので悪用して成りすますことが出来ません!

https://togetter.com/li/1314350

↑暗号化とハッシュ化の違いの違いを探したら良い資料を見つけました。

リクエスト署名検証の実装

ではリクエスト署名検証の実装について説明していきます。

まぁ公式記事の日本語化+補足付きの解説になるので、公式を理解した上で実装する場合は下記リンクを参考にしてください

https://api.slack.com/docs/verifying-requests-from-slack

SigningSecretを定義する

まずはリクエスト署名検証に使用する Signing Secret というトークンを確認します。

https://api.slack.com/apps からアプリを選択した後、『Basic Information』から確認することができます。

const SIGNING_SECRET = "08b206922f114d519245612e2ac00dc6";

タイムスタンプをチェックする

処理を行うにあたって、初めに特定のタイミングの投稿に何度もアタックされることを防ぐためにリクエストを受け付ける期間に制限を掛けます。

// TimeStampを取得
$timestamp = $request->header('x-slack-request-timestamp');

try {
    $date = new \DateTime('now');
    $now = $date->getTimestamp();
} catch (\Exception $e) {
    throw $e;
}

// 一定時間より前に送られたリクエストは弾く
if (!isset($timestamp) || !isset($now) || abs( $timestamp - $now) > self::VERIFY_LIMIT) {
    return response('Bad Request!');
}

ハッシュを受け取る

次に検証対象となるハッシュを取得します。

// Slackの投稿に紐付けられたハッシュ値を取得する
$signature = $request->header('x-slack-signature');
list($version, $signature_hash) = explode("=", $signature);
v0=[HASH]

という形式で送られてくるので分割して保存しています。

基本文字列をつくる

取得したハッシュと照合するためのハッシュを作るための文字列を作ります。

// Slackの投稿に紐付けられたハッシュ値を取得する
$body = $request->all();
$query = "";
foreach ($body as $key => $value) {
    $query .= $key . '=' . urlencode($value). '&';
}
$query = rtrim($query,'&');
$base_string = $version . ':' . $timestamp . ':' . $query;

ここで気をつけるべきポイントは文字列を作る際、リクエストのBodyに対して urlencode()を使用することです。

SlackのBodyの中にはリダイレクトURLが含まれていて、通常の文字列として使ってしまうと照合が失敗してしまいます。

また文字列を作った後に urlencode() を掛けると、 = までエンコードされてしまうため、こちらも照合が失敗してしまいます。

そのため、Bodyのパラメータを連結する中で毎回 urlencode() を掛けるようにしましょう。

基本文字列からハッシュをつくる

// Slackの投稿に紐付けられたハッシュ値を取得する
$my_signature = hash_hmac('sha256', $base_string, self::SIGNING_SECRET);

基本文字列が出来たら、ハッシュを作ります。

PHPで hash_hmac という関数が用意されているため、こちらを使用します。

https://www.php.net/manual/ja/function.hash-hmac.php

ハッシュ化方式は sha256 にして、キーワードは 一番最初に定義した SigningSecret の値を使用します。

ハッシュの比較を行う

最後に、ハッシュ値の比較を行います。

=== を使った比較でも認証は出来てしまうのですが、セキュリティ的に問題があるので hash_equals を使用しましょう。

// ハッシュ値を比較して誤っていたら不正リクエストなので終了
if (!hash_equals($signature_hash , $my_signature)) {
    return response('Bad Request!');
}

リクエスト署名検証は簡単!だから絶対やるべき!

ここまでリクエスト署名検証の方法を説明してきましたが如何でしたでしょうか?

PHPの場合、良い感じのライブラリがないため今回のような実装が必要でした。

PythonやNode.jsではSlackが公式でライブラリを出しているため、こちらを使用すると検証を行ってくれます。

Node Interactive Messages SDK
Node Events API SDK
Python Events API SDK

ぜひ参考にしてください。

ここまで読んで頂きありがとうございました!

コメント

タイトルとURLをコピーしました