Tài liệu

Bảo mật và ký HMAC

Mọi yêu cầu webhook do Savebase gửi đều được ký bằng HMAC-SHA256 để bạn có thể xác nhận yêu cầu thực sự đến từ Savebase và chưa bị sửa đổi trong quá trình truyền. Bạn luôn nên xác minh chữ ký này trước khi xử lý sự kiện webhook.

Cách ký hoạt động

Savebase tính chữ ký bằng HMAC-SHA256 trên nội dung yêu cầu thô, sử dụng giá trị webhook của bạn là Khóa bí mật ký làm khóa. Kết quả được gửi trong X-Savebase-Signature với giá trị sha256=<hex-digest>.

Dùng phần thân nguyên gốc

Luôn tính HMAC trên byte nội dung yêu cầu thô, chưa chỉnh sửa — không phải đối tượng JSON đã tuần tự hóa lại. Phân tích và chuyển JSON lại thành chuỗi có thể đổi khoảng trắng, thứ tự khóa, làm xác minh chữ ký thất bại.
Tiêu đềĐịnh dạng giá trị
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);

Thực hành bảo mật tốt

  • Phản hồi nhanh. Trả về một 200 OK trong 10 giây. Savebase hết thời gian chờ sau 10 giây và đánh dấu lần gửi thất bại. Chuyển việc nặng sang tác vụ nền.
  • Không bao giờ ghi bí mật ký vào nhật ký. Bảo vệ như mật khẩu. Đổi tại Settings → Webhooks nếu bạn cho rằng khóa đã bị lộ.
  • Chỉ dùng HTTPS. Savebase từ chối địa chỉ HTTP không mã hóa.
  • Dùng so sánh an toàn về thời gian. Luôn dùng crypto.timingSafeEqual (Node), hmac.compare_digest (Python), hoặc hash_equals (PHP) — không bao giờ dùng một ===.
  • Tự xử lý các lỗi. Savebase ghi kết quả chuyển phát, nhưng triển khai hiện tại không tự thử lại hay phát lại yêu cầu thất bại.