ドキュメント

セキュリティとHMAC署名

Savebaseが送信するすべてのWebhookリクエストにはHMAC-SHA256署名が付くため、Savebaseから送られた本物であり、転送中に改ざんされていないことを確認できます。Webhookイベントを処理する前に、必ずこの署名を検証してください。

署名の仕組み

Savebaseは、ウェブフックの次の値を鍵として、未加工のリクエスト本文にHMAC-SHA256を適用して署名を計算します。 署名シークレット を鍵に使います。結果は次に送られます: X-Savebase-Signature ヘッダーを次の形式で: sha256=<hex-digest>.

未加工本文を使う

HMACは必ず次のデータに対して計算します: 変更を加えていないリクエスト本文の生バイト列 — 再シリアライズしたJSONオブジェクトではありません。JSONを解析して再び文字列化すると、空白やキー順が変わり、署名検証に失敗する場合があります。
ヘッダー値の形式
X-Savebase-Signaturesha256=abcdef0123...
X-Savebase-Eventpost.saved
User-AgentSavebase-Webhook/1.0

Node.js / Express

bash
const crypto = require('crypto');

function verifySignature(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'utf8'),
    Buffer.from(signatureHeader, 'utf8')
  );
}

app.post(
  '/savebase-webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['x-savebase-signature'];
    if (!sig || !verifySignature(req.body, sig, process.env.SAVEBASE_WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }
    const event = JSON.parse(req.body);
    console.log('Event:', event.event, event.data.id);
    res.sendStatus(200);
  }
);

Python / Flask

bash
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['SAVEBASE_WEBHOOK_SECRET']

@app.route('/savebase-webhook', methods=['POST'])
def handle_webhook():
    sig_header = request.headers.get('X-Savebase-Signature', '')
    raw_body = request.get_data()

    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, sig_header):
        abort(401)

    payload = request.get_json()
    print('Received event:', payload['event'])
    return '', 200

PHP

bash
<?php
$secret  = getenv('SAVEBASE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_SAVEBASE_SIGNATURE'] ?? '';
$expected  = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $sigHeader)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($rawBody, true);
error_log('Received: ' . $event['event']);
http_response_code(200);

セキュリティのベストプラクティス

  • すばやく対応。 返す 200 OK を10秒以内に返してください。Savebaseは10秒でタイムアウトし、配信を失敗として記録します。重い処理はバックグラウンドジョブに移してください。
  • 署名用シークレットは絶対にログに記録しないでください。 パスワード同様に扱い、次で更新してください。 Settings → Webhooks 漏えいしたと思われる場合。
  • HTTPSのみ使用してください。 Savebaseは暗号化されていないHTTP送信先を拒否します。
  • 一定時間の安全な比較を使います。 必ず次を使用してください: crypto.timingSafeEqual (Node)、 hmac.compare_digest (Python)、または hash_equals (PHP)を使い、単純比較は絶対に使わないでください: ===.
  • 失敗時の対応は自分で行ってください。 Savebaseは配信結果を記録しますが、現在の実装は失敗したリクエストを自動再試行・再送しません。