Zabezpečení webhooků
Zabezpečení webhook endpointu je kritické pro ochranu vaší aplikace. Tato stránka popisuje mechanismy, které Klubero používá pro zajištění bezpečnosti.
HMAC podpis
Pokud při registraci webhooku nastavíte secret, každý požadavek bude obsahovat hlavičku X-Webhook-Signature s HMAC-SHA256 podpisem.
Jak podpis funguje
┌─────────────────────────────────────────────────────────────────┐
│ Vytvoření podpisu │
├─────────────────────────────────────────────────────────────────┤
│ │
│ payload = JSON tělo požadavku (jako UTF-8 string) │
│ secret = Váš webhook secret │
│ │
│ signature = HMAC-SHA256(payload, secret) │
│ │
│ Header: X-Webhook-Signature = "sha256=" + hex(signature) │
│ │
└─────────────────────────────────────────────────────────────────┘
Hlavičky pro ověření
| Hlavička | Popis |
|---|---|
X-Webhook-Signature | HMAC-SHA256 podpis ve formátu sha256=<hex> |
X-Webhook-Timestamp | Unix timestamp vytvoření požadavku |
Hlavička X-Webhook-Signature je přítomna pouze pokud má webhook nastavený secret. Pokud secret není nastaven, hlavička není odesílána.
Ověření podpisu
Krok za krokem
- Získejte hlavičku – Extrahujte hodnotu
X-Webhook-Signature - Získejte tělo – Získejte raw JSON tělo požadavku jako UTF-8 string
- Vypočítejte podpis – Vytvořte HMAC-SHA256 z těla s vaším secret klíčem
- Porovnejte – Bezpečně porovnejte vypočítaný podpis s hlavičkou
Příklad: Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
// Vypočítej očekávaný podpis
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Bezpečné porovnání (timing-safe)
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Použití v Express.js
app.post('/webhooks/klubero', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body.toString('utf8');
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(payload);
// Zpracování události...
res.status(200).send('OK');
});
Příklad: Python
import hmac
import hashlib
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
def verify_signature(payload, signature, secret):
expected_signature = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
@app.route('/webhooks/klubero', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
payload = request.get_data(as_text=True)
if not verify_signature(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
# Zpracování události...
return 'OK', 200
Příklad: C#
using System.Security.Cryptography;
using System.Text;
public class WebhookController : ControllerBase
{
private readonly string _webhookSecret;
[HttpPost("webhooks/klubero")]
public async Task<IActionResult> HandleWebhook()
{
// Získání hlavičky
var signature = Request.Headers["X-Webhook-Signature"].ToString();
// Získání raw body
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
var payload = await reader.ReadToEndAsync();
Request.Body.Position = 0;
// Ověření podpisu
if (!VerifySignature(payload, signature, _webhookSecret))
return Unauthorized("Invalid signature");
// Zpracování události...
return Ok();
}
private static bool VerifySignature(string payload, string signature, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = "sha256=" + BitConverter.ToString(hash)
.Replace("-", "")
.ToLowerInvariant();
// Timing-safe porovnání
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expectedSignature)
);
}
}
Příklad: PHP
<?php
$webhookSecret = getenv('WEBHOOK_SECRET');
function verifySignature($payload, $signature, $secret) {
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expectedSignature, $signature);
}
// Získání dat
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$payload = file_get_contents('php://input');
// Ověření
if (!verifySignature($payload, $signature, $webhookSecret)) {
http_response_code(401);
exit('Invalid signature');
}
// Zpracování události
$data = json_decode($payload, true);
// ...
http_response_code(200);
echo 'OK';
Ochrana proti replay útokům
Timestamp v hlavičce X-Webhook-Timestamp můžete použít k ochraně proti replay útokům.
Doporučená implementace
function verifyTimestamp(timestamp, toleranceSeconds = 300) {
const requestTime = parseInt(timestamp, 10);
const currentTime = Math.floor(Date.now() / 1000);
// Požadavek je příliš starý nebo v budoucnosti
return Math.abs(currentTime - requestTime) <= toleranceSeconds;
}
app.post('/webhooks/klubero', (req, res) => {
const timestamp = req.headers['x-webhook-timestamp'];
// 1. Ověření timestamp (tolerance 5 minut)
if (!verifyTimestamp(timestamp, 300)) {
return res.status(401).send('Request too old');
}
// 2. Ověření podpisu
// ...
});
Doporučujeme nastavit toleranci na 5 minut (300 sekund). To poskytuje dostatečnou rezervu pro síťové latence, ale zároveň omezuje okno pro replay útoky.
Best practices
1. Vždy ověřujte podpis
// ❌ ŠPATNĚ - bez ověření
app.post('/webhooks', (req, res) => {
processEvent(req.body);
res.send('OK');
});
// ✅ SPRÁVNĚ - s ověřením
app.post('/webhooks', (req, res) => {
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature');
}
processEvent(req.body);
res.send('OK');
});
2. Používejte timing-safe porovnání
// ❌ ŠPATNĚ - náchylné na timing útoky
if (signature === expectedSignature) { ... }
// ✅ SPRÁVNĚ - timing-safe
if (crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) { ... }
3. Bezpečně ukládejte secret
# ❌ ŠPATNĚ - v kódu
const SECRET = "muj-tajny-klic";
# ✅ SPRÁVNĚ - v environment proměnné
const SECRET = process.env.WEBHOOK_SECRET;
4. Používejte HTTPS
Klubero odesílá webhooky pouze na HTTPS endpointy. Zajistěte, že:
- Váš certifikát je platný
- Používáte silné šifrování (TLS 1.2+)
- Certifikát není self-signed
5. Logujte neúspěšné pokusy
app.post('/webhooks', (req, res) => {
if (!verifySignature(req)) {
logger.warn('Invalid webhook signature', {
ip: req.ip,
deliveryId: req.headers['x-webhook-delivery'],
timestamp: new Date().toISOString()
});
return res.status(401).send('Invalid signature');
}
// ...
});
Řešení problémů
Podpis se neshoduje
Nejčastější příčiny:
- Chybný secret – Zkontrolujte, že používáte správný secret klíč
- Modifikace body – Middleware může měnit tělo požadavku před ověřením
- Encoding – Ujistěte se, že používáte UTF-8 encoding
- Whitespace – Tělo musí být přesně ve formátu, jak bylo odesláno
// Ujistěte se, že raw body není modifikováno
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks', (req, res) => {
const rawBody = req.body.toString('utf8');
// Použijte rawBody pro ověření podpisu
});
Webhook nemá podpis
Hlavička X-Webhook-Signature je přítomna pouze pokud má webhook nastavený secret. Zkontrolujte:
- Že jste při registraci webhooku zadali
secret - Že webhook v API ukazuje
has_secret: true
Pokud potřebujete přidat secret k existujícímu webhooku, použijte PATCH endpoint:
curl -X PATCH "https://api.klubero.cz/api/v1.0/webhooks/123" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json-patch+json" \
-d '[{ "op": "replace", "path": "/secret", "value": "novy-tajny-klic" }]'