← All posts
webhook securityvibe codingAI code securitywebhook vulnerabilitiesNext.js webhook

How to Fix Insecure Webhook Endpoints in Vibe-Coded Apps

O

OverMCP Team

Quick answer

Fix insecure webhook endpoints in vibe-coded apps by validating the webhook signature using a shared secret, restricting incoming IPs to the provider's published ranges, and implementing idempotency logic to prevent duplicate processing. These three steps eliminate the most common vulnerabilities AI-generated webhook code introduces.

What is webhook security vibe coding and why it matters

When you build with AI coding tools, your webhook endpoints often arrive with missing or incorrect security checks. The AI might generate the route, parse the payload, and even return a 200—but it frequently forgets to verify who sent the request. This is the core of webhook security vibe coding: the gap between "it works" and "it's secure." Attackers can replay, spoof, or exploit unsecured webhooks to trigger unintended actions in your app, from charging customers to deleting data.

What to check first

Before diving into fixes, audit your existing webhook endpoints with this checklist:

  • [ ] Does the endpoint verify a cryptographic signature (e.g., HMAC-SHA256) from the provider?
  • [ ] Is the signature comparison constant-time to prevent timing attacks?
  • [ ] Are incoming requests restricted to the provider's IP range?
  • [ ] Does the endpoint reject requests without a valid timestamp tolerance (e.g., ±5 minutes)?
  • [ ] Is there idempotency handling (e.g., checking a unique event ID before processing)?
  • [ ] Are error responses generic (no "invalid signature" details)?
  • [ ] Is the shared secret stored as an environment variable, not hardcoded?
  • If you answered "no" to any of these, your vibe-coded webhook is vulnerable. Run a free AI app security scanner to detect common webhook misconfigurations automatically.

    Step-by-step fix

    Let's walk through securing a typical webhook endpoint in a Node.js/Next.js app built with Cursor or Bolt.new. We'll use Stripe as an example, but the pattern applies to GitHub, Clerk, Supabase, and most providers.

    1. Validate the signature

    Most providers send a signature header (e.g., stripe-signature). Your code must recompute the HMAC and compare it to the header value.

    // pages/api/webhook.js (Next.js App Router)
    import { buffer } from 'micro';
    import crypto from 'crypto';
    
    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
    
    export const config = { api: { bodyParser: false } };
    
    export default async function handler(req, res) {
      if (req.method !== 'POST') {
        return res.status(405).end();
      }
    
      const buf = await buffer(req);
      const sig = req.headers['stripe-signature'];
    
      if (!sig) {
        return res.status(400).send('Missing signature');
      }
    
      // Constant-time comparison
      const expectedSig = crypto
        .createHmac('sha256', webhookSecret)
        .update(buf.toString())
        .digest('hex');
    
      const receivedSig = sig.split(',').find(s => s.startsWith('v1='))?.split('=')[1];
    
      if (!receivedSig || !crypto.timingSafeEqual(Buffer.from(expectedSig), Buffer.from(receivedSig))) {
        return res.status(401).send('Invalid signature');
      }
    
      // Continue processing...
      res.status(200).json({ received: true });
    }

    Note: AI-generated code often uses string comparison like sig === expectedSig. This is vulnerable to timing attacks. Always use crypto.timingSafeEqual.

    2. Restrict by IP

    Add IP allowlisting as a defense-in-depth layer. Most providers publish their IP ranges (e.g., Stripe: 3.18.12.63–64). In a serverless environment like Vercel, you can check the x-forwarded-for header.

    const allowedIPs = ['3.18.12.63', '3.18.12.64']; // example only, fetch current list
    const clientIP = req.headers['x-forwarded-for']?.split(',')[0].trim();
    
    if (!allowedIPs.includes(clientIP)) {
      return res.status(403).send('Forbidden');
    }

    3. Implement idempotency

    Webhooks can be retried. Your endpoint must handle duplicate events gracefully. Use a unique event ID from the payload (e.g., stripe-event-id) and store processed IDs in a fast key-value store like Redis or a database table with a unique constraint.

    // Pseudo-code for idempotency check
    const eventId = body.id; // or header 'Idempotency-Key'
    const alreadyProcessed = await redis.get(`webhook:processed:${eventId}`);
    if (alreadyProcessed) {
      return res.status(200).send('Already processed');
    }
    await redis.set(`webhook:processed:${eventId}`, '1', 'EX', 86400); // expire after 24h

    4. Use environment variables

    Never hardcode secrets. Vibe-coded apps often inline secrets because the AI suggests them for convenience. Move all secrets to environment variables:

    # .env.local
    STRIPE_WEBHOOK_SECRET=whsec_...

    Then access via process.env.STRIPE_WEBHOOK_SECRET. Use a secret leak scanner to catch any hardcoded keys before they reach production.

    Common mistakes

    AI-generated webhook code tends to make these predictable errors:

    1. No signature verification at all

    Many Bolt.new or Lovable projects skip signature checks entirely. The AI assumes the endpoint is internal or trusts the network, leaving the door open for spoofed requests.

    2. String comparison instead of constant-time

    AI often writes if (sig === expectedSig). This leaks timing information—attackers can brute-force the signature character by character.

    3. Hardcoded secrets in the code

    AI models sometimes embed example secrets directly into the generated code. Developers copy-paste and deploy without changing them.

    4. Missing IP filtering

    Vibe-coded apps are often deployed on serverless platforms with no built-in IP restrictions. The endpoint accepts requests from any IP.

    5. No idempotency

    If a webhook is retried (e.g., Stripe retries after a timeout), the same event can trigger duplicate charges, emails, or data mutations.

    6. Verbose error messages

    Returning "Invalid signature: expected abc got xyz" gives attackers clues. Always respond with generic messages like "Unauthorized."

    How to check if your vibe-coded webhook is secure

    Run a security headers checker to verify your endpoint has proper headers, but more importantly, use a tool that tests webhook-specific vulnerabilities. OverMCP's continuous security monitoring automatically scans your deployed app for missing signature validation, exposed secrets, and other common issues—including webhook misconfigurations.

    FAQ

    How do I know if my webhook endpoint is vulnerable?

    The easiest way is to check if your endpoint verifies a signature from the provider. If there's no signature check, it's vulnerable. You can also use a free AI app security scanner to detect missing validation and other issues.

    What if my webhook provider doesn't support signatures?

    Some older or custom webhook systems don't offer signatures. In that case, use a shared secret sent via a custom header (e.g., X-Webhook-Secret) and validate it on your end. Combine with IP allowlisting and idempotency for defense in depth.

    Can I use a library to handle webhook security?

    Yes, most providers offer official libraries (e.g., stripe npm package includes stripe.webhooks.constructEvent). Using these is safer than writing your own validation. However, vibe-coded apps often miss installing or using these libraries—double-check your imports.

    ---

    By following this guide, you've closed the most critical security gaps in your vibe-coded webhook endpoints. Webhook security vibe coding doesn't have to be hard—it just requires a few deliberate steps that AI often skips. Run a quick scan now to catch any remaining issues before they become real problems.

    Is your app secure?

    Free scan in 30 seconds. No signup needed.

    Scan My App Free