Webhookセキュリティ
Omise Webhookをセキュアに実装するための完全なガイド。HMAC-SHA256署名検証、タイミング安全な比較、キー管理、リプレイ攻撃防止について説明します。
概要
Webhookセキュリティは、Omiseからのイベントが正当で改ざんされていないことを確保し、不正なリクエストから保護するために重要です。すべてのWebhookはHMAC-SHA256署名で署名されており、Webhookシークレットキーを使用して検証できます。
HMAC-SHA256署名検証
署名検証プロセス
WebhookはX-Omise-Signatureヘッダーに署名が含まれます。署名は以下のように計算されます:
signature = HMAC-SHA256(webhook_secret_key, request_body)
Node.jsでの実装
const crypto = require('crypto');
const express = require('express');
const app = express();
// Webhookシークレットキー (環境変数から)
const webhookKey = process.env.OMISE_WEBHOOK_KEY;
// Rawボディを保存
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
// シグネチャを検証する関数
function verifyWebhookSignature(body, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// タイミング安全な比較を使用
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Webhookエンドポイント
app.post('/webhooks/omise', (req, res) => {
const signature = req.headers['x-omise-signature'];
try {
// シグネチャを検証
if (!verifyWebhookSignature(req.rawBody, signature, webhookKey)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Webhookを処理
const event = req.body;
processEvent(event);
res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
function processEvent(event) {
console.log(`Processing event: ${event.key}`);
// イベント処理ロジック
}
module.exports = app;
Pythonでの実装
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
# Webhookシークレットキー (環境変数から)
WEBHOOK_KEY = os.environ.get('OMISE_WEBHOOK_KEY')
def verify_webhook_signature(body, signature, secret):
"""HMAC-SHA256署名を検証"""
expected_signature = hmac.new(
secret.encode('utf-8'),
body.encode('utf-8') if isinstance(body, str) else body,
hashlib.sha256
).hexdigest()
# タイミング安全な比較を使用
return hmac.compare_digest(signature, expected_signature)
@app.route('/webhooks/omise', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Omise-Signature', '')
payload = request.get_data(as_text=True)
try:
# シグネチャを検証
if not verify_webhook_signature(payload, signature, WEBHOOK_KEY):
print('Invalid webhook signature')
return jsonify({'error': 'Invalid signature'}), 401
# Webhookを処理
event = request.json
process_event(event)
return jsonify({'received': True}), 200
except Exception as e:
print(f'Webhook error: {e}')
return jsonify({'error': 'Internal server error'}), 500
def process_event(event):
print(f"Processing event: {event['key']}")
# イベント処理ロジック
Rubyでの実装
require 'sinatra'
require 'json'
require 'openssl'
# Webhookシークレットキー (環境変数から)
WEBHOOK_KEY = ENV['OMISE_WEBHOOK_KEY']
def verify_webhook_signature(body, signature, secret)
expected_signature = OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new('sha256'),
secret,
body
)
# タイミング安全な比較を使用
Rack::Utils.secure_compare(signature, expected_signature)
end
post '/webhooks/omise' do
signature = request.headers['X-Omise-Signature']
payload = request.body.read
request.body.rewind
begin
# シグネチャを検証
unless verify_webhook_signature(payload, signature, WEBHOOK_KEY)
puts 'Invalid webhook signature'
halt 401, { error: 'Invalid signature' }.to_json
end
# Webhookを処理
event = JSON.parse(payload)
process_event(event)
{ received: true }.to_json
rescue => e
puts "Webhook error: #{e.message}"
halt 500, { error: 'Internal server error' }.to_json
end
end
def process_event(event)
puts "Processing event: #{event['key']}"
# イベント処理ロジック
end
PHPでの実装
<?php
// Webhookシークレットキー (環境変数から)
$webhookKey = getenv('OMISE_WEBHOOK_KEY');
function verifyWebhookSignature($payload, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $payload, $secret);
// タイミング安全な比較を使用
return hash_equals($expectedSignature, $signature);
}
// Webhookエンドポイント
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_OMISE_SIGNATURE'] ?? '';
try {
// シグネチャを検証
if (!verifyWebhookSignature($payload, $signature, $webhookKey)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Webhookを処理
$event = json_decode($payload, true);
processEvent($event);
http_response_code(200);
echo json_encode(['received' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Internal server error']);
}
function processEvent($event) {
echo "Processing event: {$event['key']}\n";
// イベント処理ロジック
}
?>
Goでの実装
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
// Webhookシークレットキー (環境変数から)
var webhookKey = os.Getenv("OMISE_WEBHOOK_KEY")
func verifyWebhookSignature(body []byte, signature, secret string) bool {
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
expectedSignature := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Omise-Signature")
// ボディを読み込む
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// シグネチャを検証
if !verifyWebhookSignature(body, signature, webhookKey) {
log.Println("Invalid webhook signature")
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Webhookを処理
var event map[string]interface{}
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "Error parsing JSON", http.StatusBadRequest)
return
}
processEvent(event)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func processEvent(event map[string]interface{}) {
fmt.Printf("Processing event: %v\n", event["key"])
// イベント処理ロジック
}
func main() {
http.HandleFunc("/webhooks/omise", handleWebhook)
log.Fatal(http.ListenAndServe(":3000", nil))
}
セキュリティベストプラクティス
1. Rawボディを使用
署名を検証する場合、常にrawリクエストボディを使用してください。パースされたJSONを使用しないでください:
// ✓ 正しい
const body = req.rawBody;
// ✗ 不正
const body = JSON.stringify(req.body);
2. タイミング安全な比較を使用
タイミング攻撃を防ぐため、タイミング安全な比較関数を使用:
// ✓ 正しい - タイミング安全
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
// ✗ 不正 - タイミング攻撃に対して脆弱
signature === expected;
3. Webhookキーを安全に保存
シークレットキーを環境変数またはシークレットマネージャーに保存:
// ✓ 正しい
const key = process.env.OMISE_WEBHOOK_KEY;
// ✗ 不正
const key = 'whsec_xxxxx'; // コードに直接
4. シークレットローテーション
定期的にWebhookキーをローテーション:
// 複数キーをサポート
const primaryKey = process.env.OMISE_WEBHOOK_KEY_PRIMARY;
const secondaryKey = process.env.OMISE_WEBHOOK_KEY_SECONDARY;
function verifySignature(body, signature) {
return verifyWithKey(body, signature, primaryKey) ||
verifyWithKey(body, signature, secondaryKey);
}
5. リプレイ攻撃を防止
イベントIDとタイムスタンプをトラッキング:
function handleWebhook(event) {
const eventId = event.id;
const eventTime = event.created;
const now = Math.floor(Date.now() / 1000);
// タイムスタンプをチェック (5分以内)
if (now - eventTime > 300) {
console.error('Event is too old');
return;
}
// すでに処理済みかどうかを確認
if (processedEvents.has(eventId)) {
console.log('Duplicate event');
return;
}
processedEvents.add(eventId);
// イベントを処理
}
トラブルシューティング
シグネチャ検証に失敗
原因:
- Rawボディの代わりに解析済みJSONを使用
- 間違ったシークレットキー
- エンコーディング問題
チェック:
- Rawリクエストボディを使用していることを確認
- Webhookシークレットキーをダッシュボードから確認
- UTF-8エンコーディングを確認
セキュリティの設定ミス
ベストプラクティス:
- すべてのWebhookエンドポイントにHTTPSを使用
- Webhookシークレットキーをコミットしない
- シークレットキーをログに記録しない
- 署名検証をテストしてください
FAQ
Webhook署名キーはどこで取得できますか?
Webhook署名キーは、Omiseダッシュボードで新しいWebhookエンドポイントを作成するときに表示されます。一度しか表示されないので、安全に保存してください。紛失した場合は、新しいWebhookエンドポイントを作成してください。
テストモードと本番モードで同じWebhookキーを使用できますか?
いいえ、テストモードと本番モードは別々のWebhookエンドポイントと署名キーを持っています。各モードで異なるエンドポイントを設定してください。
署名検証が失敗した場合はどうなりますか?
HTTP 401 Unauthorizedを返します。Omiseは再試行スケジュールに従ってWebhookの配信を再試行します。
テストモードのWebhookでも署名を検証する必要がありますか?
はい、本番運用前に検証コードが正しく動作することを確認するために、テストモードと本番モードの両方で常に署名を検証してください。
Webhookキーはどのくらいの頻度でローテーションすべきですか?
6〜12ヶ月ごと、または 漏洩の疑いがある場合は直ちにキーをローテーションしてください。ダウンタイムを避けるために、デュアルキー検証を実装してください。
署名検証の代わりにOmiseのIPアドレスをホワイトリストに登録できますか?
いいえ、IPホワイトリストだけでは不十分です。IPアドレスはスプーフィングされる可能性があり、変更される可能性もあるため、常に署名を検証してください。
リプレイ攻撃を検出した場合はどうすればよいですか?
イベントをログに記録し、再試行を防ぐために200 OKで応答し、パターンを監視します。攻撃が継続する場合はセキュリティチームに警告してください。
処理済みのイベントIDはどのくらいの期間保存すべきですか?
少なくとも7日間(Omiseの再試行期間)イベントIDを保存してください。より長い保持期間(30日)でより良い保護が得られます。
署名検証を同期的に行うことはできますか?
はい、署名検証は高速です(数ミリ秒)。Webhookに応答する前に同期的に検証してください。
Webhook キーが漏洩した場合はどうすればよいですか?
直ちに新しいキーで新しいWebhookエンドポイントを作成し、アプリケーションを更新し、漏洩したエンドポイントを無効化してください。
関連リソース
- Webhookセットアップ - Webhookの構成
- 再試行ロジック - イベント再試行とべき等
- Webhookテスト - Webhookテスト
- API認証 - APIセキュリティ
次のステップ
- HMAC-SHA256署名検証を実装
- タイミング安全な比較を使用
- Webhookキーを安全に保存
- リプレイ攻撃防止を実装
- 定期的にシークレットキーをローテーション
- Webhookセキュリティをテスト