Segurança
Verificação e Segurança
A assinatura HMAC é obrigatória. Sem validá-la, qualquer um que descubra a URL do webhook pode forjar eventos de pagamento.
Header:
X-HyzePay-Signature: t=<timestamp>,v1=<hex>. Use o raw body — nunca re-serialize o JSON.Como calculamos
- Pegamos o timestamp Unix em segundos (
t) - Montamos a string:
`${t}.${rawBody}`rawBody= corpo exato do POST (bytes UTF-8, sem re-serializar) signature = HMAC_SHA256(secret, signedPayload)em hex- Header:
X-HyzePay-Signature: t=<timestamp>,v1=<signature>
Como validar no seu servidor
- Leia o body cru (raw string)
- Extraia
tev1do header - Rejeite se
|now - t| > 300segundos (5 min) — evita replay - Recalcule o HMAC com o mesmo secret do painel (
whsec_…) - Compare com
timingSafeEqual(nunca===em produção se puder)
Node.js / TypeScript
import { createHmac, timingSafeEqual } from "crypto";
export function verifyHyzePaySignature(opts: {
secret: string;
rawBody: string;
signatureHeader: string;
toleranceSec?: number;
}): boolean {
const tolerance = opts.toleranceSec ?? 300;
const parts = opts.signatureHeader.split(",").map((p) => p.trim());
let t: number | null = null;
const signatures: string[] = [];
for (const part of parts) {
const [k, v] = part.split("=");
if (k === "t" && v) t = Number(v);
if (k === "v1" && v) signatures.push(v);
}
if (t == null || !Number.isFinite(t) || signatures.length === 0) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > tolerance) return false;
const expected = createHmac("sha256", opts.secret)
.update(`${t}.${opts.rawBody}`, "utf8")
.digest("hex");
const expectedBuf = Buffer.from(expected, "utf8");
return signatures.some((sig) => {
try {
const got = Buffer.from(sig, "utf8");
return got.length === expectedBuf.length && timingSafeEqual(got, expectedBuf);
} catch {
return false;
}
});
}Express (raw body)
import express from "express";
const app = express();
// Importante: capturar raw body ANTES do json parser global
app.post(
"/webhooks/hyzepay",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const sig = req.header("x-hyzepay-signature") || "";
if (!verifyHyzePaySignature({
secret: process.env.HYZEPAY_WEBHOOK_SECRET!,
rawBody,
signatureHeader: sig,
})) {
return res.status(401).json({ error: "invalid_signature" });
}
const event = JSON.parse(rawBody);
// … processar …
return res.status(200).json({ received: true });
},
);Next.js App Router
// app/api/webhooks/hyzepay/route.ts
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const rawBody = await req.text();
const sig = req.headers.get("x-hyzepay-signature") || "";
if (!verifyHyzePaySignature({
secret: process.env.HYZEPAY_WEBHOOK_SECRET!,
rawBody,
signatureHeader: sig,
})) {
return NextResponse.json({ error: "invalid_signature" }, { status: 401 });
}
const event = JSON.parse(rawBody);
// v2: event.type + event.data.object
// v1: event.event + event.data
return NextResponse.json({ received: true });
}Python
import hmac, hashlib, time
def verify_hyzepay_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
t = None
signatures = []
for part in header.split(","):
part = part.strip()
if part.startswith("t="):
t = int(part[2:])
elif part.startswith("v1="):
signatures.append(part[3:])
if t is None or not signatures:
return False
if abs(int(time.time()) - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)PHP
function verifyHyzePaySignature(string $secret, string $rawBody, string $header, int $tolerance = 300): bool {
$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
$part = trim($part);
if (str_starts_with($part, 't=')) $t = (int) substr($part, 2);
if (str_starts_with($part, 'v1=')) $signatures[] = substr($part, 3);
}
if ($t === null || count($signatures) === 0) return false;
if (abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
foreach ($signatures as $sig) {
if (hash_equals($expected, $sig)) return true;
}
return false;
}