Вебхуки выглядят как самая скучная часть интеграции: пришёл POST, распарсил, ответил 200. Но в e-commerce через них ходят деньги и заказы — от маркетплейсов, от платёжных провайдеров. И проверка подписи там — это место, где легко сделать "почти правильно", что на самом деле неправильно.
Три грабли, на которые я сам наступал или видел рядом:
1. Сравнение подписи через
== или bytes.Equal. Это не constant-time, и теоретически подпись можно подобрать по утечкам времени. Надо hmac.Equal.2. Нет проверки таймстемпа — значит, перехваченный запрос можно переиграть хоть через сутки. Окно в 5 минут закрывает 99% проблем.
3. Парсинг JSON до проверки подписи ломает всё: ты re-сериализуешь тело, порядок ключей и пробелы меняются, и подпись уже не сходится. Хешировать надо ровно те байты, что пришли.
Примерно так:
body, _ := io.ReadAll(r.Body)
ts := r.Header.Get("X-Signature-Timestamp")
unix, _ := strconv.ParseInt(ts, 10, 64)
if time.Since(time.Unix(unix, 0)).Abs() > 5*time.Minute {
return errors.New("stale")
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(body)
got, _ := hex.DecodeString(r.Header.Get("X-Signature"))
if !hmac.Equal(got, mac.Sum(nil)) {
return errors.New("bad sig")
}
Мало кода, много смысла. И главное — сначала проверяем байты, потом парсим JSON, а не наоборот.