Webhook 安全对于确保发送到你端点的请求确实来自 Firecrawl,且未被篡改,至关重要。本页介绍如何验证 webhook 的真实性并落实安全最佳实践。

为什么 Webhook 安全至关重要

如果缺乏适当的验证,攻击者可能会:
  • 发送伪造的 webhook 请求以触发不必要的 actions
  • 篡改负载数据以操纵你的应用
  • 通过大量请求使你的 webhook 端点过载

Firecrawl 如何对 Webhook 进行签名

Firecrawl 使用你账户的密钥并通过 HMAC-SHA256 对每个 webhook 请求进行签名。这样会为每个请求生成唯一的签名,以证明:
  1. 请求来自 Firecrawl
  2. 有效负载未被篡改

查找你的密钥

你的 webhook 密钥可在账户设置中的 Advanced 选项卡 找到。每个账户都有一个唯一的密钥,用于为所有 webhook 请求签名。
请妥善保管你的 webhook 密钥,切勿公开泄露。如果你认为 密钥已遭泄露,请立即在账户设置中重新生成。

签名校验

签名的工作原理

每个 webhook 请求都会包含一个名为 X-Firecrawl-Signature 的请求头,格式如下:
X-Firecrawl-Signature: sha256=abc123def456...
签名的计算方式如下:
  1. 取原始请求体(JSON 字符串)
  2. 使用你的密钥生成 HMAC-SHA256 哈希
  3. 转为十六进制字符串
  4. 在前面加上 sha256=

实现示例

import crypto from 'crypto';
import express from 'express';

const app = express();

// 使用原始请求体解析器进行签名校验
app.use('/webhook/firecrawl', express.raw({ type: 'application/json' }));

app.post('/webhook/firecrawl', (req, res) => {
  const signature = req.get('X-Firecrawl-Signature');
  const webhookSecret = process.env.FIRECRAWL_WEBHOOK_SECRET;
  
  if (!signature || !webhookSecret) {
    return res.status(401).send('未授权');
  }
  
  // 从签名头中提取哈希值
  const [algorithm, hash] = signature.split('=');
  if (algorithm !== 'sha256') {
    return res.status(401).send('签名算法无效');
  }
  
  // 计算期望的签名
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(req.body)
    .digest('hex');
  
  // 使用恒定时间比较进行签名校验
  if (!crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(expectedSignature, 'hex'))) {
    return res.status(401).send('签名无效');
  }
  
  // 解析并处理已校验的 webhook
  const event = JSON.parse(req.body);
  console.log('已校验的 Firecrawl webhook:', event);
  
  res.status(200).send('ok');
});

app.listen(3000, () => console.log('监听端口 3000'));

分步验证

  1. X-Firecrawl-Signature 头中提取签名
  2. 获取原始请求体(按收到的原始字节,不要先解析)
  3. 使用你的密钥和原始请求体计算 HMAC-SHA256
  4. 使用时间安全的比较函数比较签名
  5. 仅当签名匹配时才处理该 webhook

安全最佳实践

始终验证签名

切勿信任未经签名验证的 webhook 请求:
// ❌ 不佳 - 未验证
app.post('/webhook', (req, res) => {
  processWebhook(req.body); // 危险!
  res.status(200).send('OK');
});

// ✅ 良好 - 先验证
app.post('/webhook', (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('未授权');
  }
  processWebhook(req.body);
  res.status(200).send('OK');
});

使用时间常数/计时安全的比较

标准字符串比较可能泄露时间信息。请使用专用函数:
  • Node.jscrypto.timingSafeEqual()
  • Pythonhmac.compare_digest()
  • 其他语言:查找“constant-time”或“timing-safe”的比较函数

强制使用 HTTPS

对 webhook 始终使用 HTTPS 端点:
{
  "url": "https://your-app.com/webhook" // ✅ 安全
}
{
  "url": "http://your-app.com/webhook" // ❌ 不安全
}