跳到内容

Webhook签名验证:为什么HMAC-SHA256是信任支付回调的唯一安全方式

Payora7 分钟阅读EN · RU · UK · ES · DE · FR · IT · PL · PT · HI · ZH

一个支付Webhook到达您的服务器,声称订单已支付。您信任它吗?如果您仅根据HTTP调用来确认订单——而不验证加密签名——您就给攻击者提供了一个免费的方式来将任何订单标记为已支付。

没有验证会出什么错

一个未保护的 webhook 端点很容易被利用。攻击者只需要知道你的回调 URL——这通常可以从浏览器的网络标签、公共 GitHub 仓库或测试结账中发现——然后他们就可以 POST 一个精心制作的有效负载:

POST /webhook HTTP/1.1
Content-Type: application/json

{"order_id": "1337", "status": "paid", "amount": "99.99"}

如果你的处理程序读取$_POST['status'] === 'paid'并且确认订单,攻击者就免费购买了价值 $99.99 的商品。这不是一个理论攻击;它经常发生在未保护的支付集成上。

HMAC-SHA256 签名的工作原理

HMAC(基于哈希的消息认证码)与 SHA-256 通过使有效负载在没有秘密密钥的情况下无法伪造来解决这个问题。过程是:

  1. 支付网关知道你的api_secret——这是你和网关之间共享的秘密。
  2. 在发送 webhook 之前,它会计算:HMAC-SHA256(timestamp + "." + body, api_secret)
  3. 它在 HTTP 头中包含此签名和时间戳。
  4. 您的服务器重新计算相同的 HMAC 并进行比较。如果匹配,则有效负载是可信的。

一个不知道您 api_secret 的攻击者无法伪造有效签名 — SHA-256 是单向函数。时间戳防止重放攻击:即使是有效的拦截 webhook,在 5 分钟后也变得无用。

实现验证:PHP

function verifyPayoraWebhook(string $secret): array {
    $ts   = $_SERVER['HTTP_X_PAYORA_TIMESTAMP'] ?? '';
    $sig  = $_SERVER['HTTP_X_PAYORA_SIGNATURE'] ?? '';
    $body = file_get_contents('php://input');

    // 1. reject stale timestamps (replay protection)
    if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) {
        http_response_code(400); exit('stale timestamp');
    }

    // 2. recompute and compare — hash_equals() prevents timing attacks
    $expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
    if (!hash_equals($expected, $sig)) {
        http_response_code(401); exit('invalid signature');
    }

    return json_decode($body, true);
}

$event = verifyPayoraWebhook('sk_your_secret');
if ($event['status'] === 'paid') {
    creditOrder($event['order_id']);
}

实现验证:Node.js

import crypto from 'node:crypto';

function verifyPayoraWebhook(req, rawBody, secret) {
    const ts  = req.headers['x-payora-timestamp'] ?? '';
    const sig = req.headers['x-payora-signature'] ?? '';

    if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
        return null; // stale
    }

    const expected = crypto
        .createHmac('sha256', secret)
        .update(`${ts}.${rawBody}`)
        .digest('hex');

    // timingSafeEqual prevents timing side-channel attacks
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
        ? JSON.parse(rawBody)
        : null;
}

破坏验证的常见错误

1. 读取解析后的主体而不是原始字节。 Express 的 json()中间件重新序列化 JSON,这可能会重新排序键或更改空格。始终缓冲原始请求体之前解析,并将其用于 HMAC。

2. 使用===而不是时间安全比较。简单的字符串比较会泄露时间信息,这可能被利用来逐字猜测签名字节。使用hash_equals()在 PHP 中或crypto.timingSafeEqual()在 Node 中。

3. 跳过时间戳检查。一个有效但过时的 webhook 被网络观察者捕获后可能会被重放。5分钟的窗口(±300秒)消除了这种风险,同时处理合理的时钟漂移。

4. 在验证之前记录原始主体。如果您的日志管道被破坏,记录的有效负载可能会被重放。先验证,后记录。

超越签名:幂等性

验证证明 webhook 是真实的;幂等性确保您不会重复计入订单。网络在非 200 响应时重试 webhook,因此您的处理程序可能会多次接收到相同的事件。解决方案是在您的数据库中记录一个 webhook_id ,如果您之前见过它,则跳过处理:

// pseudo-code
$id = $event['webhook_id'];
if (DB::exists('processed_webhooks', $id)) return respond(200);
DB::insert('processed_webhooks', $id);
creditOrder($event['order_id']);

结合签名验证和幂等性,您可以获得一个既安全又可靠的支付集成。查看完整的集成指南 以获取 Payora 的完整 webhook 参考。

Comments

准备开始了吗?

创建账户,并在一小时内开出第一张账单。

此页面回答的问题