メインコンテンツへスキップ

ウェブフック セキュリティ

Omise ウェブフック をセキュアに実装するための完全なガイド。HMAC-SHA256 署名検証、タイミング安全な比較、キー管理、リプレイ攻撃防止について説明します。

概要

ウェブフック セキュリティ は、Omise からのイベント が正当で改ざんされていないことを確保し、不正なリクエスト から保護するために重要です。すべてのウェブフック は HMAC-SHA256 署名で署名されており、ウェブフック シークレット キー を使用して検証 できます。

HMAC-SHA256 署名検証

署名検証 プロセス

ウェブフック は X-Omise-Signature ヘッダー に署名 が含まれます。署名 は以下のように計算 されます:

signature = HMAC-SHA256(webhook_secret_key, request_body)

Node.js での実装

const crypto = require('crypto');
const express = require('express');
const app = express();

// ウェブフック シークレット キー (環境変数 から)
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)
);
}

// ウェブフック エンドポイント
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' });
}

// ウェブフック を処理
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_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

# ウェブフック を処理
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_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

# ウェブフック を処理
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
// ウェブフック シークレット キー (環境変数 から)
$webhookKey = getenv('OMISE_WEBHOOK_KEY');

function verifyWebhookSignature($payload, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $payload, $secret);

// タイミング安全な比較を使用
return hash_equals($expectedSignature, $signature);
}

// ウェブフック エンドポイント
$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;
}

// ウェブフック を処理
$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"
)

// ウェブフック シークレット キー (環境変数 から)
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
}

// ウェブフック を処理
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. ウェブフック キー を安全に保存

シークレット キー を環境変数 またはシークレット マネージャー に保存:

// ✓ 正しい
const key = process.env.OMISE_WEBHOOK_KEY;

// ✗ 不正
const key = 'whsec_xxxxx'; // コードに直接

4. シークレット ローテーション

定期的 にウェブフック キー をローテーション:

// 複数 キー をサポート
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 を使用
  • 間違ったシークレット キー
  • エンコーディング 問題

チェック:

  1. Raw リクエスト ボディ を使用 していることを確認
  2. ウェブフック シークレット キー をダッシュボード から確認
  3. UTF-8 エンコーディング を確認

セキュリティ の設定ミス

ベストプラクティス:

  • すべてのウェブフック エンドポイント に HTTPS を使用
  • ウェブフック シークレット キー をコミット しない
  • シークレット キー をログ に記録しない
  • 署名検証 をテスト してください

関連リソース

次のステップ

  1. HMAC-SHA256 署名検証 を実装
  2. タイミング安全な比較 を使用
  3. ウェブフック キー を安全に保存
  4. リプレイ攻撃防止 を実装
  5. 定期的 にシークレット キー をローテーション
  6. ウェブフック セキュリティ をテスト