الوثائق

الأمان وتوقيع HMAC

يُوقّع كل طلب webhook يرسله Savebase باستخدام 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 نتيجة التسليم، لكن التنفيذ الحالي لا يعيد المحاولة أو تشغيل الطلبات الفاشلة تلقائيًا.