Security & Authentication

Secure your webhook endpoints with signature validation and best practices for handling Statused webhook requests

Always secure your webhook endpoints to ensure only genuine requests from Statused are processed. When configuring your webhook in Statused, you can provide an optional secret key used to generate an HMAC-SHA256 signature included in the webhook headers.

Always use secrets in production. Without signature validation, malicious actors could potentially send fake webhook requests to your endpoints.

Common Security Issues

Missing Signature Validation

Problem: Accepting all webhook requests without verification allows malicious actors to send fake requests.

Solution: Always validate the X-Statused-Signature header using HMAC-SHA256 signature verification.

webhook.js
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const secret = process.env.WEBHOOK_SECRET;
  const signature = req.headers['x-statused-signature'];

  if (!signature) {
    return res.status(401).send('Missing signature');
  }

  const expectedSignature = 'sha256=' +
    crypto.createHmac('sha256', secret)
          .update(req.body)
          .digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook
  const payload = JSON.parse(req.body);
  res.status(200).send('OK');
});
webhooks_controller.rb
class WebhookController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :validate_signature

  def receive
    # Process the webhook
    payload = JSON.parse(request.raw_post)
    render json: { status: 'ok' }, status: :ok
  end

  private

  def validate_signature
    secret = ENV['WEBHOOK_SECRET']
    signature = request.headers['X-Statused-Signature']

    if signature.blank?
      render json: { error: 'Missing signature' }, status: :unauthorized
      return
    end

    expected_signature = 'sha256=' +
      OpenSSL::HMAC.hexdigest('SHA256', secret, request.raw_post)

    unless ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
      render json: { error: 'Invalid signature' }, status: :unauthorized
    end
  end
end
views.py
@csrf_exempt
@require_http_methods(["POST"])
def webhook(request):
    if not validate_signature(request.body, request.META.get('HTTP_X_STATUSED_SIGNATURE')):
        return HttpResponseBadRequest("Invalid signature")

    # Process the webhook
    payload = request.body
    return HttpResponse("OK")

def validate_signature(payload, signature):
    if not signature:
        return False

    secret = os.environ['WEBHOOK_SECRET']
    expected_signature = 'sha256=' + hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

Timing Attacks

Problem: Using simple string comparison for signatures allows attackers to determine the correct signature through timing analysis.

Solution: Use timing-safe comparison functions that take constant time regardless of input differences.

webhook.js
const crypto = require('crypto');

function validateSignature(payload, signature, secret) {
  const expectedSignature = 'sha256=' +
    crypto.createHmac('sha256', secret)
          .update(payload)
          .digest('hex');

  // ❌ Bad: Vulnerable to timing attacks
  // return signature === expectedSignature;

  // ✅ Good: Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
webhooks_controller.rb
def validate_signature
  secret = ENV['WEBHOOK_SECRET']
  signature = request.headers['X-Statused-Signature']
  expected_signature = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', secret, request.raw_post)

  # ❌ Bad: Vulnerable to timing attacks
  # unless signature == expected_signature

  # ✅ Good: Timing-safe comparison
  unless ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
    render json: { error: 'Unauthorized' }, status: :unauthorized
  end
end
views.py
import hmac

def validate_signature(payload, signature):
    if not signature:
        return False

    secret = os.environ['WEBHOOK_SECRET']
    expected_signature = 'sha256=' + hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    # ❌ Bad: Vulnerable to timing attacks
    # return signature == expected_signature

    # ✅ Good: Timing-safe comparison
    return hmac.compare_digest(signature, expected_signature)

Verbose Error Messages

Problem: Returning detailed error information to webhook requests can leak sensitive information about your system.

Solution: Log detailed errors internally for debugging, but return simple status codes to external requests.

webhook.js
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  try {
    const signature = req.headers['x-statused-signature'];
    const secret = process.env.WEBHOOK_SECRET;

    if (!signature) {
      // ✅ Good: Log details internally, return simple response
      console.error('Webhook request missing signature header', {
        ip: req.ip,
        userAgent: req.get('User-Agent')
      });
      return res.status(401).send('Unauthorized');
    }

    if (!validateSignature(req.body, signature, secret)) {
      // ✅ Good: Log security event
      console.error('Invalid webhook signature', {
        ip: req.ip,
        signature: signature,
        expectedStart: expectedSignature.substring(0, 10)
      });
      return res.status(401).send('Unauthorized');
    }

    // Process webhook
    const payload = JSON.parse(req.body);
    res.status(200).send('OK');

  } catch (error) {
    // ✅ Good: Log error details internally
    console.error('Webhook processing error:', error);

    // ❌ Bad: return res.status(500).send(`Error: ${error.message}`);
    // ✅ Good: Return simple error response
    res.status(500).send('Internal Server Error');
  }
});
webhooks_controller.rb
class WebhookController < ApplicationController
  def receive
    begin
      validate_signature

      # Process webhook
      payload = JSON.parse(request.raw_post)
      Rails.logger.info "Processed webhook: #{payload['type']}"

      render json: { status: 'ok' }, status: :ok

    rescue JSON::ParserError => e
      # ✅ Good: Log details internally
      Rails.logger.error "Webhook JSON parsing failed: #{e.message}", {
        ip: request.remote_ip,
        body_preview: request.raw_post[0..100]
      }

      # ❌ Bad: render json: { error: "JSON parsing failed: #{e.message}" }
      # ✅ Good: Return simple error
      render json: { error: 'Bad Request' }, status: :bad_request

    rescue => e
      Rails.logger.error "Webhook processing error: #{e.message}"
      render json: { error: 'Internal Server Error' }, status: :internal_server_error
    end
  end

  private

  def validate_signature
    signature = request.headers['X-Statused-Signature']

    if signature.blank?
      Rails.logger.warn "Webhook missing signature", { ip: request.remote_ip }
      render json: { error: 'Unauthorized' }, status: :unauthorized
      return
    end

    # Validation logic...
  end
end
views.py
import logging
import json

logger = logging.getLogger(__name__)

@csrf_exempt
@require_http_methods(["POST"])
def webhook(request):
    try:
        signature = request.META.get('HTTP_X_STATUSED_SIGNATURE')

        if not signature:
            # ✅ Good: Log security event
            logger.warning('Webhook missing signature', extra={
                'ip': request.META.get('REMOTE_ADDR'),
                'user_agent': request.META.get('HTTP_USER_AGENT')
            })
            return HttpResponseBadRequest("Unauthorized")

        if not validate_signature(request.body, signature):
            logger.warning('Invalid webhook signature', extra={
                'ip': request.META.get('REMOTE_ADDR')
            })
            return HttpResponseBadRequest("Unauthorized")

        # Process webhook
        payload = json.loads(request.body)
        logger.info(f"Processed webhook: {payload.get('type')}")

        return HttpResponse("OK")

    except json.JSONDecodeError as e:
        # ✅ Good: Log details internally
        logger.error(f"Webhook JSON parsing failed: {e}")

        # ❌ Bad: return HttpResponseBadRequest(f"JSON error: {e}")
        # ✅ Good: Return simple error
        return HttpResponseBadRequest("Bad Request")

    except Exception as e:
        logger.error(f"Webhook processing error: {e}")
        return HttpResponseServerError("Internal Server Error")

Additional Security Best Practices

  • Rate Limiting: Implement reasonable rate limits on your webhook endpoints to prevent abuse.
  • Monitoring: Set up alerts for failed webhook deliveries and security events.
  • Idempotency: Handle duplicate webhook deliveries gracefully since Statused will retry failed deliveries.

Need help securing your webhooks?

Security is critical for webhook implementations. If you need assistance with setup or have questions about best practices, reach out to us — we're here to help keep your integrations secure.