Token导航 LogoToken导航

Agent Skill · 前端设计

webhook-security(网络钩子安全)

用于辅助安全审计、权限检查、凭据风险、认证流程和常见漏洞排查。它适合让 Agent 梳理敏感配置、检查依赖风险、分析鉴权逻辑或生成安全复核清单。使用时不能把工具输出直接当最终结论,涉及密钥、令牌、用户数据或生产系统时,应先确认最小权限、脱敏方式和操作边界。

webhook-security 是前端设计类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为权限未标注;许可证信息还不明确,第三方扫描有提醒项,按安装量在前端设计类的 10,216 个 Skill 里排第 4,439。

总安装 888周安装 37GitHub Stars 777下载 296核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:webhook-security(网络钩子安全)
来源仓库:https://github.com/dadbodgeoff/drift
仓库路径:skills/webhook-security
安装命令:
npx skills add https://github.com/dadbodgeoff/drift --skill webhook-security
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/dadbodgeoff/drift --skill webhook-security

这个 Skill 是做什么的

前端设计类 Skill 关注页面结构、视觉规范和组件写法,输出通常是可运行的界面代码。

webhook-security 用于安全接收外部 POST 请求,适合在 Codex、Claude、Cursor、Gemini CLI 中需要处理支付或第三方通知时使用。它采用 HMAC-SHA256 签名验证与时间戳防重放,提供多层防御与速率限制集成。

使用时需校验签名密钥时效性与载荷完整性,避免伪造请求;建议隔离处理线程。安装前请确认密钥存储方式,注意是否会暴露敏感头信息,确保传输与存储均加密。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

来源仓库内路径:skills/webhook-security

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/dadbodgeoff/drift --skill webhook-security;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    权限没有明确标注,第一次使用时只给一个无关紧要的任务,观察它到底读了哪些文件和网络。

常见问题

6 条

webhook-security 怎么安装?

在终端执行 npx skills add https://github.com/dadbodgeoff/drift --skill webhook-security。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

webhook-security 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

webhook-security 需要什么权限?

权限级别没有明确标注。第一次使用时建议只给无关紧要的任务,观察它实际读写了哪些内容。

webhook-security 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

webhook-security 用的人多吗?

公开数据是累计安装 888 次,近一周安装 37 次,下载 296 次,GitHub 777 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

webhook-security 安全吗?

第三方扫描有提醒项,说明存在需要人工确认的行为。安装前请打开源码看清它读了什么、联网到哪里。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方扫描存在提醒项或未通过项,需要人工逐条确认。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理前端设计类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

前端设计类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
frontend-design

前端设计 · Codex、Claude

8,823,360126,072只读MIT通过
vercel-composition-patterns

Vercel 组合模式 · Codex、Claude

3,858,81625,893需要联网MIT通过
vercel-react-native-skills

Vercel React native skills 前端 · Codex、Claude

2,468,66425,878只读MIT通过
adapt

adapt 搜索 · Codex、Claude

2,027,04023,125操作浏览器未明确通过
bolder

bolder 分析 · Codex、Claude

2,004,28823,130只读未明确通过
webhook-security

网络钩子安全 · 本页

888777权限未标注未明确有提醒

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Webhook Security

Production-ready webhook handling with defense in depth.

When to Use This Skill

  • Receiving webhooks from payment providers (Stripe, PayPal)
  • Integrating with GitHub, GitLab, or other dev tools
  • Building your own webhook delivery system
  • Any endpoint receiving external POST requests

Security Layers

┌─────────────────────────────────────────────────────┐
│                    Incoming Webhook                  │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│  1. Signature Verification (HMAC-SHA256)            │
│     - Reject if signature invalid                   │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│  2. Timestamp Validation                            │
│     - Reject if older than 5 minutes                │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│  3. Idempotency Check                               │
│     - Skip if already processed                     │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│  4. Process Webhook                                 │
│     - Handle business logic                         │
└─────────────────────────────────────────────────────┘

TypeScript Implementation

Signature Verification

// webhook-verifier.ts
import crypto from 'crypto';

interface WebhookConfig {
  secret: string;
  signatureHeader: string;
  timestampHeader?: string;
  tolerance?: number; // seconds
}

interface VerificationResult {
  valid: boolean;
  error?: string;
}

class WebhookVerifier {
  constructor(private config: WebhookConfig) {}

  verify(payload: string | Buffer, headers: Record<string, string>): VerificationResult {
    const signature = headers[this.config.signatureHeader.toLowerCase()];

    if (!signature) {
      return { valid: false, error: 'Missing signature header' };
    }

    // Check timestamp if configured
    if (this.config.timestampHeader) {
      const timestamp = headers[this.config.timestampHeader.toLowerCase()];
      if (!timestamp) {
        return { valid: false, error: 'Missing timestamp header' };
      }

      const timestampAge = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
      const tolerance = this.config.tolerance || 300; // 5 minutes default

      if (Math.abs(timestampAge) > tolerance) {
        return { valid: false, error: 'Timestamp outside tolerance window' };
      }
    }

    // Compute expected signature
    const expectedSignature = this.computeSignature(payload, headers);

    // Constant-time comparison to prevent timing attacks
    const valid = crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );

    return { valid, error: valid ? undefined : 'Invalid signature' };
  }

  private computeSignature(payload: string | Buffer, headers: Record<string, string>): string {
    const timestamp = this.config.timestampHeader
      ? headers[this.config.timestampHeader.toLowerCase()]
      : '';

    const signedPayload = timestamp ? `${timestamp}.${payload}` : payload.toString();

    return 'sha256=' + crypto
      .createHmac('sha256', this.config.secret)
      .update(signedPayload)
      .digest('hex');
  }
}

export { WebhookVerifier, WebhookConfig, VerificationResult };

Provider-Specific Verifiers

// providers/stripe.ts
import Stripe from 'stripe';

export function verifyStripeWebhook(
  payload: string | Buffer,
  signature: string,
  secret: string
): Stripe.Event {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

  // Stripe's library handles verification
  return stripe.webhooks.constructEvent(payload, signature, secret);
}

// providers/github.ts
import crypto from 'crypto';

export function verifyGitHubWebhook(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// providers/twilio.ts
import crypto from 'crypto';

export function verifyTwilioWebhook(
  url: string,
  params: Record<string, string>,
  signature: string,
  authToken: string
): boolean {
  // Twilio uses URL + sorted params
  const data = url + Object.keys(params)
    .sort()
    .map(key => key + params[key])
    .join('');

  const expected = crypto
    .createHmac('sha1', authToken)
    .update(data)
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Idempotency Handler

// idempotency.ts
import { Redis } from 'ioredis';

interface IdempotencyConfig {
  redis: Redis;
  keyPrefix?: string;
  ttlSeconds?: number;
}

class IdempotencyHandler {
  private redis: Redis;
  private keyPrefix: string;
  private ttl: number;

  constructor(config: IdempotencyConfig) {
    this.redis = config.redis;
    this.keyPrefix = config.keyPrefix || 'webhook:processed:';
    this.ttl = config.ttlSeconds || 86400; // 24 hours
  }

  async isProcessed(eventId: string): Promise<boolean> {
    const key = this.keyPrefix + eventId;
    const exists = await this.redis.exists(key);
    return exists === 1;
  }

  async markProcessed(eventId: string, result?: unknown): Promise<void> {
    const key = this.keyPrefix + eventId;
    const value = JSON.stringify({
      processedAt: new Date().toISOString(),
      result,
    });
    await this.redis.setex(key, this.ttl, value);
  }

  async getProcessedResult(eventId: string): Promise<unknown | null> {
    const key = this.keyPrefix + eventId;
    const value = await this.redis.get(key);
    if (!value) return null;
    return JSON.parse(value);
  }
}

export { IdempotencyHandler, IdempotencyConfig };

Complete Webhook Handler

// webhook-handler.ts
import { Request, Response, NextFunction } from 'express';
import { WebhookVerifier } from './webhook-verifier';
import { IdempotencyHandler } from './idempotency';

interface WebhookHandlerConfig {
  verifier: WebhookVerifier;
  idempotency: IdempotencyHandler;
  eventIdExtractor: (payload: unknown) => string;
}

function createWebhookHandler(config: WebhookHandlerConfig) {
  return async (req: Request, res: Response, next: NextFunction) => {
    // Get raw body (must use raw body parser)
    const rawBody = req.body;

    // 1. Verify signature
    const verification = config.verifier.verify(
      rawBody,
      req.headers as Record<string, string>
    );

    if (!verification.valid) {
      console.error('Webhook verification failed:', verification.error);
      return res.status(401).json({ error: verification.error });
    }

    // Parse payload
    const payload = JSON.parse(rawBody.toString());

    // 2. Check idempotency
    const eventId = config.eventIdExtractor(payload);

    if (await config.idempotency.isProcessed(eventId)) {
      console.log(`Webhook ${eventId} already processed, skipping`);
      return res.status(200).json({ status: 'already_processed' });
    }

    // 3. Attach parsed payload and continue
    req.body = payload;
    (req as any).webhookEventId = eventId;

    // 4. After processing, mark as processed
    res.on('finish', async () => {
      if (res.statusCode >= 200 && res.statusCode < 300) {
        await config.idempotency.markProcessed(eventId);
      }
    });

    next();
  };
}

export { createWebhookHandler };

Python Implementation

# webhook_security.py
import hmac
import hashlib
import time
from typing import Optional, Dict, Any
from dataclasses import dataclass
import redis

@dataclass
class VerificationResult:
    valid: bool
    error: Optional[str] = None

class WebhookVerifier:
    def __init__(
        self,
        secret: str,
        signature_header: str,
        timestamp_header: Optional[str] = None,
        tolerance: int = 300,
    ):
        self.secret = secret
        self.signature_header = signature_header.lower()
        self.timestamp_header = timestamp_header.lower() if timestamp_header else None
        self.tolerance = tolerance

    def verify(self, payload: bytes, headers: Dict[str, str]) -> VerificationResult:
        # Normalize headers to lowercase
        headers = {k.lower(): v for k, v in headers.items()}

        signature = headers.get(self.signature_header)
        if not signature:
            return VerificationResult(False, "Missing signature header")

        # Check timestamp
        if self.timestamp_header:
            timestamp = headers.get(self.timestamp_header)
            if not timestamp:
                return VerificationResult(False, "Missing timestamp header")

            timestamp_age = abs(int(time.time()) - int(timestamp))
            if timestamp_age > self.tolerance:
                return VerificationResult(False, "Timestamp outside tolerance")

        # Compute expected signature
        expected = self._compute_signature(payload, headers)

        # Constant-time comparison
        valid = hmac.compare_digest(signature, expected)

        return VerificationResult(valid, None if valid else "Invalid signature")

    def _compute_signature(self, payload: bytes, headers: Dict[str, str]) -> str:
        timestamp = headers.get(self.timestamp_header, "") if self.timestamp_header else ""
        signed_payload = f"{timestamp}.{payload.decode()}" if timestamp else payload

        if isinstance(signed_payload, str):
            signed_payload = signed_payload.encode()

        signature = hmac.new(
            self.secret.encode(),
            signed_payload,
            hashlib.sha256
        ).hexdigest()

        return f"sha256={signature}"

class IdempotencyHandler:
    def __init__(
        self,
        redis_client: redis.Redis,
        key_prefix: str = "webhook:processed:",
        ttl_seconds: int = 86400,
    ):
        self.redis = redis_client
        self.key_prefix = key_prefix
        self.ttl = ttl_seconds

    def is_processed(self, event_id: str) -> bool:
        key = f"{self.key_prefix}{event_id}"
        return self.redis.exists(key) == 1

    def mark_processed(self, event_id: str, result: Any = None) -> None:
        import json
        key = f"{self.key_prefix}{event_id}"
        value = json.dumps({
            "processed_at": time.time(),
            "result": result,
        })
        self.redis.setex(key, self.ttl, value)

FastAPI Middleware

# fastapi_webhook.py
from fastapi import Request, HTTPException, Depends
from functools import wraps

def webhook_protected(
    verifier: WebhookVerifier,
    idempotency: IdempotencyHandler,
    event_id_extractor: callable,
):
    async def dependency(request: Request):
        # Get raw body
        body = await request.body()

        # Verify signature
        result = verifier.verify(body, dict(request.headers))
        if not result.valid:
            raise HTTPException(status_code=401, detail=result.error)

        # Parse and check idempotency
        import json
        payload = json.loads(body)
        event_id = event_id_extractor(payload)

        if idempotency.is_processed(event_id):
            raise HTTPException(status_code=200, detail="Already processed")

        # Store for later marking
        request.state.webhook_event_id = event_id
        request.state.webhook_payload = payload

        return payload

    return Depends(dependency)

# Usage
@app.post("/webhooks/stripe")
async def stripe_webhook(
    payload: dict = webhook_protected(
        verifier=stripe_verifier,
        idempotency=idempotency_handler,
        event_id_extractor=lambda p: p["id"],
    )
):
    # Process webhook
    event_type = payload["type"]
    # ...

    # Mark as processed
    idempotency_handler.mark_processed(payload["id"])
    return {"status": "ok"}

Express Setup

// Important: Use raw body parser for webhooks
import express from 'express';

const app = express();

// Regular JSON parser for most routes
app.use(express.json());

// Raw body parser for webhook routes
app.use('/webhooks', express.raw({ type: 'application/json' }));

// Webhook route with verification
app.post('/webhooks/stripe',
  createWebhookHandler({
    verifier: stripeVerifier,
    idempotency: idempotencyHandler,
    eventIdExtractor: (p: any) => p.id,
  }),
  async (req, res) => {
    const event = req.body;

    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckout(event.data.object);
        break;
      // ... other handlers
    }

    res.json({ received: true });
  }
);

Building Your Own Webhook Sender

// webhook-sender.ts
import crypto from 'crypto';

interface WebhookDelivery {
  url: string;
  event: string;
  payload: unknown;
  secret: string;
}

async function sendWebhook(delivery: WebhookDelivery): Promise<boolean> {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const body = JSON.stringify(delivery.payload);

  // Create signature
  const signedPayload = `${timestamp}.${body}`;
  const signature = 'sha256=' + crypto
    .createHmac('sha256', delivery.secret)
    .update(signedPayload)
    .digest('hex');

  try {
    const response = await fetch(delivery.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': signature,
        'X-Webhook-Timestamp': timestamp,
        'X-Webhook-Event': delivery.event,
        'X-Webhook-ID': crypto.randomUUID(),
      },
      body,
    });

    return response.ok;
  } catch (error) {
    console.error('Webhook delivery failed:', error);
    return false;
  }
}

Best Practices

  1. Always verify signatures: Never trust unverified payloads
  2. Use constant-time comparison: Prevents timing attacks
  3. Check timestamps: Prevents replay attacks
  4. Implement idempotency: Same webhook may be delivered multiple times
  5. Use raw body parser: JSON parsing before verification breaks signatures
  6. Return 200 quickly: Process async to avoid timeouts

Common Mistakes

  • Parsing JSON before signature verification
  • Using regular string comparison for signatures
  • Not handling duplicate deliveries
  • Blocking on webhook processing (causes retries)
  • Exposing webhook secrets in logs

Security Checklist

  • Signature verification with HMAC-SHA256
  • Constant-time signature comparison
  • Timestamp validation (5 min tolerance)
  • Idempotency handling
  • Raw body parser for webhook routes
  • Secrets in environment variables
  • HTTPS only
  • Logging without exposing secrets

平台分布

Codex

36.56%
按下载量换算108

Claude

26.76%
按下载量换算79

Cursor

18.85%
按下载量换算56

Gemini CLI

9.19%
按下载量换算27

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

权限未标注

当前来源未能明确判断权限范围,默认进入异常复核队列。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在提醒或未通过项,安装前请自行复核。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills