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.
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');
});
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
@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.
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)
);
}
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
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.
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');
}
});
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
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.