WE

webhook-security

Implements security best practices for receiving third-party webhooks to prevent spoofing and duplicate processing.

Install

mkdir -p .claude/skills/webhook-security && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/4905" && unzip -o skill.zip -d .claude/skills/webhook-security && rm skill.zip

Installs to .claude/skills/webhook-security

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Implement secure webhook handling with signature verification, replay protection, and idempotency. Use when receiving webhooks from third-party services like Stripe, GitHub, Twilio, or building your own webhook system.
218 chars✓ has a “when” trigger
Advanced

Key capabilities

  • Verify HMAC-SHA256 signatures
  • Validate request timestamps
  • Implement idempotency checks
  • Prevent replay attacks
  • Perform constant-time comparisons

How it works

It implements a multi-layer security pipeline including signature verification, timestamp validation, and idempotency checks to ensure secure webhook processing.

Inputs & outputs

You give it
Raw request body and headers
You get back
Verified payload or error response

When to use webhook-security

  • Verifying Stripe webhook signatures
  • Implementing idempotency keys
  • Adding replay protection to API listeners
  • Validating incoming request security

About this skill

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):
         

---

*Content truncated.*

When not to use it

  • Parsing JSON before verification
  • Exposing secrets in logs

Prerequisites

Redis for idempotencyEnvironment variables for secrets

Limitations

  • Requires raw body access
  • Strict 5-minute timestamp tolerance

How it compares

It enforces a defense-in-depth approach, specifically preventing common vulnerabilities like timing attacks and replay attacks.

Compared to similar skills

webhook-security side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
webhook-security (this skill)16moCautionAdvanced
api-security-best-practices156moReviewIntermediate
api-security-hardening05moReviewIntermediate
file-uploads46moNo flagsAdvanced

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

api-security-best-practices

davila7

Implement secure API design patterns including authentication, authorization, input validation, rate limiting, and protection against common API vulnerabilities

1554

api-security-hardening

aj-geddes

>

00

file-uploads

davila7

Expert at handling file uploads and cloud storage. Covers S3, Cloudflare R2, presigned URLs, multipart uploads, and image optimization. Knows how to handle large files without blocking. Use when: file upload, S3, R2, presigned URL, multipart.

438

graphql

davila7

GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.

624

solana-dev

mashharuki

Use when user asks to "build a Solana dapp", "write an Anchor program", "create a token", "debug Solana errors", "set up wallet connection", "test my Solana program", "deploy to devnet", or "explain Solana concepts" (rent, accounts, PDAs, CPIs, etc.). End-to-end Solana development playbook covering

00

solana-dev

solanabr

Unified skill hub for Solana development. Routes to external submodule skills (solana-foundation, sendai, solana-game, trailofbits, cloudflare, qedgen, colosseum) and local skills. Progressive disclosure — read only what you need.

00

Search skills

Search the agent skills registry