보안 및 HMAC 서명
Savebase가 보내는 모든 웹훅 요청에는 HMAC-SHA256 서명이 있어 실제 Savebase에서 보냈는지, 전송 중 변조되지 않았는지 확인할 수 있습니다. 웹훅 이벤트를 처리하기 전에 항상 이 서명을 검증해야 합니다.
서명 작동 방식
Savebase는 웹훅의 다음 값을 사용하여 원시 요청 본문에 HMAC-SHA256을 실행해 서명을 계산합니다: 서명 비밀키 키로 사용합니다. 결과는 다음으로 전송됩니다. X-Savebase-Signature 헤더에 다음 형식으로 sha256=<hex-digest>.
원본 본문 사용
HMAC는 항상 다음 데이터를 기준으로 계산하세요: 수정하지 않은 원본 요청 본문의 바이트 — 다시 직렬화한 JSON 객체가 아닙니다. 파싱 후 문자열화하면 공백과 키 순서가 달라져 서명 검증이 실패할 수 있습니다.
| 헤더 | 값 형식 |
|---|---|
X-Savebase-Signature | sha256=abcdef0123... |
X-Savebase-Event | post.saved |
User-Agent | Savebase-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 '', 200PHP
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 OK10초 안에 응답하세요. Savebase는 10초 후 실패로 처리합니다. 무거운 작업은 백그라운드로 넘기세요. - 서명 비밀 키를 로그에 기록하지 마세요. 비밀번호처럼 다루세요. 다음에서 갱신하세요.
Settings → Webhooks유출되었다고 생각되면. - HTTPS만 사용하세요. Savebase는 일반 HTTP 엔드포인트를 거부합니다.
- 타이밍 공격에 안전한 비교를 사용하세요. 항상 사용하세요:
crypto.timingSafeEqual(Node),hmac.compare_digest(Python) 또는hash_equals(PHP) — 일반 형식은 사용하지 마세요:===. - 실패는 직접 처리하세요. Savebase는 전달 결과를 기록하지만 현재 구현에서는 실패한 요청을 자동 재시도하거나 재실행하지 않습니다.