Security Considerations¶
This document outlines security controls, known risks, and recommendations for the core-commerce package.
Authentication & Authorisation¶
API Authentication¶
| Endpoint Type | Authentication Method | Notes |
|---|---|---|
Webhooks (/api/webhooks/*) |
HMAC signature | Gateway-specific verification |
Billing API (/api/commerce/*) |
Laravel auth middleware |
Session/Sanctum token |
| Provisioning API | Bearer token (planned) | Currently commented out |
Webhook Security¶
Both payment gateways use HMAC signature verification:
BTCPay:
// Signature in BTCPay-Sig header
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
hash_equals($expectedSignature, $providedSignature);
Stripe:
// Uses Stripe SDK signature verification
\Stripe\Webhook::constructEvent($payload, $signature, $webhookSecret);
Current Gaps¶
-
No idempotency enforcement - Webhook handlers check order state (
isPaid()) but don't store processed event IDs. Replay attacks within the state-check window are possible. -
No IP allowlisting - Webhook endpoints accept connections from any IP. Consider adding gateway IP ranges to allowlist.
-
Rate limiting is global - Current throttle (
120,1) applies globally, not per-IP. A malicious actor could exhaust the limit.
Data Protection¶
Sensitive Data Handling¶
| Data Type | Storage | Protection |
|---|---|---|
| Card details | Never stored | Handled by gateways via redirect |
| Gateway API keys | Environment variables | Not in codebase |
| Webhook secrets | Environment variables | Used for HMAC |
| Tax IDs (VAT numbers) | Encrypted column recommended | Currently plain text |
| Billing addresses | Database JSON column | Consider encryption |
PCI DSS Compliance¶
The commerce module is designed to be PCI DSS SAQ A compliant:
- No card data ever touches Host UK servers
- Checkout redirects to hosted payment pages (BTCPay/Stripe)
- Only tokenized references (customer IDs, payment method IDs) are stored
- No direct card number input in application
GDPR Considerations¶
Personal data in commerce models:
- orders.billing_name, billing_email, billing_address
- invoices.billing_* fields
- referrals.ip_address, user_agent
Recommendations: - Implement data export for billing history (right of access) - Add retention policy for old orders/invoices - Hash or truncate IP addresses after 90 days - Document lawful basis for processing (contract performance)
Input Validation¶
Current Controls¶
// Coupon codes normalized
$data['code'] = strtoupper($data['code']);
// Order totals calculated server-side
$taxResult = $this->taxService->calculateForOrderable($orderable, $taxableAmount);
$total = $subtotal - $discountAmount + $setupFee + $taxResult->taxAmount;
// Gateway responses logged without sensitive data
protected function sanitiseErrorMessage($response): string
Validation Gaps¶
- Billing address structure - Accepted as array without schema validation
- Coupon code length - No maximum length enforcement
- Metadata fields - JSON columns accept arbitrary structure
Recommendations¶
// Add validation rules
$rules = [
'billing_address.line1' => ['required', 'string', 'max:255'],
'billing_address.city' => ['required', 'string', 'max:100'],
'billing_address.country' => ['required', 'string', 'size:2'],
'billing_address.postal_code' => ['required', 'string', 'max:20'],
'coupon_code' => ['nullable', 'string', 'max:32', 'alpha_dash'],
];
Transaction Security¶
Idempotency¶
Order creation supports idempotency keys:
if ($idempotencyKey) {
$existingOrder = Order::where('idempotency_key', $idempotencyKey)->first();
if ($existingOrder) {
return $existingOrder;
}
}
Gap: Webhooks don't use idempotency. Add WebhookEvent lookup:
if (WebhookEvent::where('idempotency_key', $event['id'])->exists()) {
return response('Already processed', 200);
}
Race Conditions¶
Identified risks:
- Concurrent subscription operations - Pause/unpause/cancel without locks
- Coupon redemption -
incrementUsage()without atomic check - Payout requests - Commission assignment without row locks
Mitigation: Add FOR UPDATE locks or use atomic operations:
// Use DB::transaction with locking
$commission = ReferralCommission::lockForUpdate()
->where('id', $commissionId)
->where('status', 'matured')
->first();
Amount Verification¶
Current state: BTCPay webhook trusts order total without verifying against gateway response.
Risk: Under/overpayment handling undefined.
Recommendation:
$settledAmount = $invoiceData['raw']['amount'] ?? null;
if ($settledAmount !== null && abs($settledAmount - $order->total) > 0.01) {
Log::warning('Payment amount mismatch', [
'order_total' => $order->total,
'settled_amount' => $settledAmount,
]);
// Handle partial payment or overpayment
}
Fraud Prevention¶
Current Controls¶
- Checkout session TTL (30 minutes default)
- Rate limiting on API endpoints
- Idempotency keys for order creation
Missing Controls¶
- Velocity checks - No detection of rapid-fire order attempts
- Geo-blocking - No IP geolocation validation against billing country
- Card testing detection - No small-amount charge pattern detection
- Device fingerprinting - No device/browser tracking
Recommendations¶
// Add CheckoutRateLimiter to createCheckout
$rateLimiter = app(CheckoutRateLimiter::class);
if (!$rateLimiter->attempt($workspace->id)) {
throw new TooManyCheckoutAttemptsException();
}
// Consider Stripe Radar for card payments
'stripe' => [
'radar_enabled' => true,
'block_threshold' => 75, // Block if risk score > 75
],
Audit Logging¶
What's Logged¶
- Order status changes via
LogsActivitytrait - Subscription status changes via
LogsActivitytrait - Webhook events via
WebhookLoggerservice - Payment failures and retries
What's Not Logged¶
- Failed authentication attempts on billing API
- Coupon validation failures
- Tax ID validation API calls
- Admin actions on refunds/credit notes
Recommendations¶
Add audit events for:
// Sensitive operations
activity('commerce')
->causedBy($admin)
->performedOn($refund)
->withProperties(['reason' => $reason])
->log('Refund processed');
Secrets Management¶
Environment Variables¶
# Gateway credentials
BTCPAY_URL=https://pay.host.uk.com
BTCPAY_STORE_ID=xxx
BTCPAY_API_KEY=xxx
BTCPAY_WEBHOOK_SECRET=xxx
STRIPE_KEY=pk_xxx
STRIPE_SECRET=sk_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Tax API credentials
COMMERCE_EXCHANGE_RATE_API_KEY=xxx
Key Rotation¶
No automated key rotation currently implemented.
Recommendations: - Store credentials in secrets manager (AWS Secrets Manager, HashiCorp Vault) - Implement webhook secret rotation with grace period - Alert on API key exposure in logs
Security Checklist¶
Before Production¶
- [ ] Webhook secrets are unique per environment
- [ ] Rate limiting tuned for expected traffic
- [ ] Error messages don't leak internal details
- [ ] API keys not in version control
- [ ] SSL/TLS required for all endpoints
Ongoing¶
- [ ] Monitor webhook failure rates
- [ ] Review failed payment patterns weekly
- [ ] Audit refund activity monthly
- [ ] Update gateway SDKs quarterly
- [ ] Penetration test annually
Incident Response¶
Compromised API Key¶
- Revoke key immediately in gateway dashboard
- Generate new key
- Update environment variable
- Restart application
- Audit recent transactions for anomalies
Webhook Secret Leaked¶
- Generate new secret in gateway
- Update both old and new in config (grace period)
- Monitor for invalid signature attempts
- Remove old secret after 24 hours
Suspected Fraud¶
- Pause affected subscription
- Flag orders for manual review
- Contact gateway for chargeback advice
- Document in incident log
Third-Party Dependencies¶
Gateway SDKs¶
| Package | Version | Security Notes |
|---|---|---|
stripe/stripe-php |
^12.0 | Keep updated for security patches |
Other Dependencies¶
spatie/laravel-activitylog- Audit loggingbarryvdh/laravel-dompdf- PDF generation (ensure no user input in HTML)
Dependency Audit¶
Run regularly:
Contact¶
Report security issues to: security@host.uk.com
Do not open public issues for security vulnerabilities.