Implement rate limiting on webhook endpoints #199

Closed
opened 2026-02-02 17:27:11 +00:00 by jason.woltje · 0 comments
Owner

Problem

Webhook endpoint has no rate limiting, allowing spam or denial-of-service attacks.

Location

apps/api/src/stitcher/stitcher.controller.ts:19-22

@Post("webhook")
async webhook(@Body() payload: WebhookPayloadDto): Promise<JobDispatchResult> {
  return this.stitcherService.handleWebhook(payload);
  // ❌ No rate limiting
  // ❌ No request signature validation
  // ❌ No source IP allowlist
}

Impact

  • DoS attacks via webhook flooding
  • Job queue exhaustion
  • No authentication of webhook source
  • No protection against replay attacks

Acceptance Criteria

  • Implement rate limiting (max 100/min per IP)
  • Add webhook signature validation (HMAC)
  • Add IP allowlist for webhook sources
  • Add request deduplication
  • Log all webhook attempts
  • Add monitoring for rate limit hits
  • Document webhook security

Implementation

@Post("webhook")
@UseGuards(WebhookSignatureGuard)  // NEW: Validate HMAC
@UseInterceptors(RateLimitInterceptor)  // NEW: 100/min per IP
async webhook(@Body() payload: WebhookPayloadDto) {
  return this.stitcherService.handleWebhook(payload);
}

// Signature validation
@Injectable()
export class WebhookSignatureGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    const signature = req.headers['x-webhook-signature'];
    const body = JSON.stringify(req.body);
    
    const expected = crypto
      .createHmac('sha256', process.env.WEBHOOK_SECRET)
      .update(body)
      .digest('hex');
    
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  }
}

// Rate limiting
@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
  async intercept(context: ExecutionContext, next: CallHandler) {
    const req = context.switchToHttp().getRequest();
    const ip = req.ip;
    
    const count = await this.redis.incr(`ratelimit:webhook:${ip}`);
    if (count === 1) {
      await this.redis.expire(`ratelimit:webhook:${ip}`, 60);
    }
    
    if (count > 100) {
      throw new TooManyRequestsException('Rate limit exceeded');
    }
    
    return next.handle();
  }
}

Testing

  • Test rate limit enforced
  • Test invalid signature rejected
  • Test duplicate requests detected
  • Test allowlist IP bypass

References

M4.2-Infrastructure verification report (2026-02-02)

## Problem Webhook endpoint has no rate limiting, allowing spam or denial-of-service attacks. ## Location apps/api/src/stitcher/stitcher.controller.ts:19-22 ```typescript @Post("webhook") async webhook(@Body() payload: WebhookPayloadDto): Promise<JobDispatchResult> { return this.stitcherService.handleWebhook(payload); // ❌ No rate limiting // ❌ No request signature validation // ❌ No source IP allowlist } ``` ## Impact - DoS attacks via webhook flooding - Job queue exhaustion - No authentication of webhook source - No protection against replay attacks ## Acceptance Criteria - [ ] Implement rate limiting (max 100/min per IP) - [ ] Add webhook signature validation (HMAC) - [ ] Add IP allowlist for webhook sources - [ ] Add request deduplication - [ ] Log all webhook attempts - [ ] Add monitoring for rate limit hits - [ ] Document webhook security ## Implementation ```typescript @Post("webhook") @UseGuards(WebhookSignatureGuard) // NEW: Validate HMAC @UseInterceptors(RateLimitInterceptor) // NEW: 100/min per IP async webhook(@Body() payload: WebhookPayloadDto) { return this.stitcherService.handleWebhook(payload); } // Signature validation @Injectable() export class WebhookSignatureGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const req = context.switchToHttp().getRequest(); const signature = req.headers['x-webhook-signature']; const body = JSON.stringify(req.body); const expected = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET) .update(body) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } } // Rate limiting @Injectable() export class RateLimitInterceptor implements NestInterceptor { async intercept(context: ExecutionContext, next: CallHandler) { const req = context.switchToHttp().getRequest(); const ip = req.ip; const count = await this.redis.incr(`ratelimit:webhook:${ip}`); if (count === 1) { await this.redis.expire(`ratelimit:webhook:${ip}`, 60); } if (count > 100) { throw new TooManyRequestsException('Rate limit exceeded'); } return next.handle(); } } ``` ## Testing - [ ] Test rate limit enforced - [ ] Test invalid signature rejected - [ ] Test duplicate requests detected - [ ] Test allowlist IP bypass ## References M4.2-Infrastructure verification report (2026-02-02)
jason.woltje added this to the M4.2-Infrastructure (0.0.4) milestone 2026-02-02 17:27:11 +00:00
jason.woltje added the securityp2apiapi labels 2026-02-02 17:27:11 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaic/stack#199