Slackからのリクエストの署名検証をする方法
Slack から送られるリクエスト(例えば、Slash Commands や Interactive Components など)を安全に取り扱うためには、Slack 側が本当に送ったリクエストであることを検証する仕組みが必要です。そのために 「署名検証 (signature verification)」 を実装します。
この記事では、以下のポイントを中心に解説します。
- なぜ署名検証が必要なのか?
- 署名検証の具体的な手順
- リプレイ攻撃を防ぐためのタイムスタンプ検証
- 実際の実装例
もし Slack アプリを作成して外部向けにエンドポイントを公開している方は、ぜひ参考にしてみてください。
なぜ署名検証が必要なのか?
Slack のリクエストに対して署名を検証する理由は、「このリクエストが本当に Slack から送られたものかを確かめる」ためです。インターネット上に公開されたエンドポイントには、悪意のある第三者からのリクエストが送られる可能性があります。何らかの方法でリクエストボディを偽装されると、アプリ側で不正に処理が行われる危険性があります。
そこで、Slack では署名検証という仕組みを提供し、
- Slack から送信されるリクエストには固有の署名が含まれている
- その署名をサーバーサイドで再計算し、Slack からのリクエストかどうかを検証する
という方法でリクエストの正当性を保証しています。
また、署名を検証するだけでなく、リプレイ攻撃を防ぐために、「リクエストが一定時間内のものか」をチェックすることも推奨されています。これは、過去のリクエストを使い回した攻撃を無効化するために重要なステップになります。
署名検証の具体的な手順
Slack 公式ドキュメントにも記載されていますが、署名検証は主に以下のステップで行われます。
-
タイムスタンプの取得
Slack が送った HTTP ヘッダーからX-Slack-Request-Timestamp
を取得します。これが署名を作るとき、またリプレイ攻撃を防ぐときに必要になります。 -
タイムスタンプが一定時間以内かどうかをチェック
一般的には “5 分” 以内(300 秒以内)に収まるリクエストかどうかを確認します。大きくずれている場合は古いリクエストと見なし、処理を拒否します。 -
署名用の文字列 (sigBasestring) を作成
v0:${timestamp}:${rawBody}
の形式で文字列を作ります。-
timestamp
: Slack から送られる UNIX 時間(秒単位) -
rawBody
: リクエストのボディ(文字列で取得したもの)
-
-
HMAC-SHA256 を使って署名を作成
上記の文字列を、Slack アプリ設定画面などで取得できるSigning Secret
を使って HMAC-SHA256 ハッシュ化します。その結果を 16 進数に変換した文字列の先頭にv0=
をつけたものが独自の署名 (mySignature
) となります。 -
Slack が送ってきた署名との比較
Slack 側が送ってきた署名 (X-Slack-Signature
) と、自分で作った署名 (mySignature
) が一致するかを比較します。タイミング攻撃を防ぐために、crypto.timingSafeEqual
が推奨されています。
リプレイ攻撃とは何か
そもそもリプレイ攻撃とは、過去に正当な送信者から送られた通信データを、攻撃者が盗聴・保存してあとから再送する(使い回す)ことで、受信側を騙し不正な処理を行わせる手法のことです。暗号化や署名によって正しいデータであっても、一度送ったものを繰り返し「再送」される可能性があるため、タイムスタンプチェックやワンタイムトークンなどを用いて防御を行う必要があります。
リプレイ攻撃への対策
上記のステップ 2 でも触れたとおり、Slack は 5 分以内 に送信されたリクエストのみ有効とすることを推奨しています。もし署名が正しくても、古いタイムスタンプのリクエストを受け入れてしまうと、過去に使われたリクエストが再送されるリプレイ攻撃を受けてしまう可能性があります。
そのため、timestamp
が現在時刻から 5 分以上ずれていれば署名検証を false として拒否する実装が必要です。
実装例
以下は TypeScript で書かれた署名検証のサンプルコードです。解説をコードコメント内にも記載しています。
import crypto from "crypto";
/**
* Slackからのリクエストの署名を検証する関数
*
* リクエストが有効であり、リプレイ攻撃でないことを確認する
*
* @param params 検証に必要なパラメータ
* @param params.timestamp リクエストのタイムスタンプ(秒単位)
* @param params.signature リクエストの署名
* @param params.rawBody リクエストの生のボディ(文字列)
* @param params.slackSigningSecret Slackの署名検証に使用するシークレットキー
* @returns 署名が有効であれば `true`、無効であれば `false`
*/
export async function verifySlackSignature({
timestamp,
signature,
rawBody,
slackSigningSecret,
}: {
timestamp: string | undefined;
signature: string | undefined;
rawBody: string;
slackSigningSecret: string;
}): Promise<boolean> {
// timestampやsignatureがなければNG
if (!timestamp || !signature) {
return false;
}
// 現在時刻(秒単位)
const time = Math.floor(Date.now() / 1000);
// リプレイ攻撃防止のため、5分以上前/先のリクエストは除外
if (Math.abs(time - Number.parseInt(timestamp, 10)) > 60 * 5) {
return false;
}
// 署名作成用の文字列
const sigBasestring = `v0:${timestamp}:${rawBody}`;
// 自分側の署名(v0=...)を作成
const mySignature = `v0=${crypto
.createHmac("sha256", slackSigningSecret)
.update(sigBasestring, "utf8")
.digest("hex")}`;
try {
// timingSafeEqualで比較(タイミング攻撃対策)
return crypto.timingSafeEqual(
new Uint8Array(Buffer.from(mySignature, "utf8")),
new Uint8Array(Buffer.from(signature, "utf8")),
);
} catch (error) {
// 万が一エラーが出た場合も安全にfalseを返しておく
return false;
}
}
解説
timestamp と signature は Slack から送信される HTTP ヘッダー (X-Slack-Request-Timestamp と X-Slack-Signature) から取得します。
time は現在時刻を秒単位で取得しており、過去 5 分以上経過しているかどうかをチェックしています。
sigBasestring に v0:${timestamp}:${rawBody}
をセットし、これを HMAC-SHA256 でハッシュ化しています。
Slack 側から送られる署名は v0=... という形式のため、ハッシュ化結果に v0= をつけた文字列と照合します。
比較には crypto.timingSafeEqual を用いて、タイミング情報からハッシュ値の違いを推測される攻撃を防いでいます。
まとめ
Slack からのリクエストを安全に処理するには、署名検証が必須です。加えてリプレイ攻撃を防ぐためにタイムスタンプのチェックも忘れずに実装しましょう。今回紹介したロジックは比較的シンプルですが、セキュリティ面でとても重要な役割を果たします。
- リクエストが本当に Slack から送られたかどうかを検証する
- 過去のリクエストを使い回されないようにする
これらを実践することで、アプリケーションの安全性を高めることができます。Slack アプリを作った際に役立ててもらえらた嬉しいです。
もし詳しい情報を知りたい場合は、Slack 公式ドキュメント も合わせてご覧ください。
Discussion