Skip to content

Security Architecture

UberLotto implements a 3-layer defense-in-depth security model spanning network, application, and data layers. All security-related types and enums are defined in the canonical source file app/lib/security-types.ts.

Security Layers

┌──────────────────────────────────────────────────────────────────┐
│                        SECURITY LAYERS                            │
├──────────────────────────────────────────────────────────────────┤
│                                                                    │
│  Layer 1: NETWORK                                                 │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │  • Supabase Network Restrictions (IP Whitelisting)        │    │
│  │  • Cloudflare DDoS Protection (via Shopify Oxygen)        │    │
│  │  • HTTPS/TLS Encryption                                   │    │
│  └──────────────────────────────────────────────────────────┘    │
│                              │                                     │
│                              ▼                                     │
│  Layer 2: APPLICATION                                             │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │  • Rate Limiting (per-txn, per-IP, global circuit breaker)│    │
│  │  • Input & Amount Validation                              │    │
│  │  • CSRF Protection (origin-based)                         │    │
│  │  • Webhook HMAC Signature Verification                    │    │
│  │  • Endpoint Authentication (Shopify Customer Account)    │    │
│  │  • CORS Origin Restriction (getAllowedOrigin)            │    │
│  └──────────────────────────────────────────────────────────┘    │
│                              │                                     │
│                              ▼                                     │
│  Layer 3: DATA                                                    │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │  • Row Level Security (RLS) in PostgreSQL                 │    │
│  │  • Replay Attack Prevention (nonce tracking)              │    │
│  │  • GDPR-Compliant Security Event Logging                  │    │
│  │  • Data Encryption at Rest (Supabase-managed)             │    │
│  │  • DB-Level Triggers (immutable completed, max events)   │    │
│  └──────────────────────────────────────────────────────────┘    │
│                                                                    │
└──────────────────────────────────────────────────────────────────┘

Network Security

Supabase Network Restrictions

Direct PostgreSQL connections are restricted to whitelisted IP addresses only.

TypeCIDRsDescription
Developer IP178.148.227.175/32Local development access
Cloudflare IPv415 rangesShopify Oxygen edge servers
Cloudflare IPv67 rangesShopify Oxygen edge servers

Important Limitation

Network Restrictions only apply to direct PostgreSQL connections. They do NOT restrict:

  • REST API (PostgREST)
  • Auth API
  • Storage API

For API security, use Row Level Security (RLS) policies.

Cloudflare IP Ranges (Whitelisted)

IPv4:

173.245.48.0/20    103.21.244.0/22    103.22.200.0/22
103.31.4.0/22      141.101.64.0/18    108.162.192.0/18
190.93.240.0/20    188.114.96.0/20    197.234.240.0/22
198.41.128.0/17    162.158.0.0/15     104.16.0.0/13
104.24.0.0/14      172.64.0.0/13      131.0.72.0/22

IPv6:

2400:cb00::/32    2606:4700::/32    2803:f800::/32
2405:b500::/32    2405:8100::/32    2a06:98c0::/29
2c0f:f248::/32

Managing Restrictions

bash
# View current restrictions
supabase network-restrictions --project-ref <ref> get --experimental

# Update restrictions
supabase network-restrictions --project-ref <ref> update \
  --db-allow-cidr YOUR_IP/32 \
  --experimental

Application Security

Rate Limiting

File: app/lib/rate-limiter.server.ts

Three-tier in-memory rate limiting with sliding window algorithm:

TierScopeLimitWindow
TransactionPer transaction ID10 req1 min
IP AddressPer client IP100 req1 min
Global Circuit BreakerAll requests1000 req1 min

Additional invoice creation limits (enforced in api.plisio-invoice.ts):

  • Max 5 pending transactions per user email
  • Max 3 new invoices per minute per user email

See Rate Limiting Configuration for full details.

Webhook Security

Files:

  • app/lib/webhook-validator.server.ts — HMAC verification, IP whitelisting
  • app/lib/webhook-processor.server.ts — Business logic, amount/currency validation
  • app/lib/webhook-extractor.server.ts — Multi-format data extraction
  • app/lib/replay-protection.server.ts — Nonce-based replay prevention

HMAC Signature Verification

Plisio webhooks are verified using HMAC-SHA1 with PHP serialize format:

Correction

The signature comparison uses a custom XOR-based constantTimeEqual() function, NOT crypto.timingSafeEqual. This is because the Web Crypto API used in Cloudflare Workers does not provide timingSafeEqual.

typescript
// Custom constant-time comparison (XOR-based)
private constantTimeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return result === 0;
}

MoonPay webhooks use HMAC-SHA256 with timestamp freshness validation:

  • Header format: Moonpay-Signature-V2: t=TIMESTAMP,s=SIGNATURE
  • Signed payload: TIMESTAMP.BODY (timestamp + literal period + raw body)
  • Signature encoding: hex
  • Timestamp freshness: rejects webhooks older than 5 minutes
  • Replay protection: nonce-based via checkGenericWebhookNonce() using shared webhook_nonces table with 5-minute TTL
  • Same XOR-based constant-time comparison pattern as Plisio

See Webhook Security Deep Dive for full pipeline details.

Endpoint Authentication

Several payment API endpoints require Shopify Customer Account authentication via customerAccount.isLoggedIn(). Unauthenticated requests receive a 401 Unauthorized response.

EndpointAuth Required
POST /api/plisio-invoiceYes — customerAccount.isLoggedIn()
GET /api/plisio-statusYes — customerAccount.isLoggedIn()
GET/POST /api/wallet-transactionsYes — customerAccount.isLoggedIn()
POST /api/moonpay-signYes — customerAccount.isLoggedIn()
POST /api/shopify-checkoutYes — customerAccount.handleAuthStatus()
POST /api/moonpay-webhookNo (server-to-server, HMAC-verified)
GET/POST /api/plisio-webhookNo (server-to-server, HMAC-verified)

CORS Origin Restriction

File: app/lib/cors.server.ts

All payment API endpoints use a shared getAllowedOrigin() function that restricts CORS origins:

  • Development: Allows localhost and 127.0.0.1 (any port)
  • Production: Only allows the app's own origin (same-origin)

Requests from disallowed origins receive no Access-Control-Allow-Origin header (or 403 Forbidden on OPTIONS preflight). This replaces the previous wildcard * CORS policy.

Endpoints using getAllowedOrigin(): wallet-transactions, plisio-invoice, plisio-status, moonpay-sign.

Input Validation

File: app/lib/amount-validator.server.ts

All payment amounts are validated against:

  • NaN / Infinity rejection
  • Scientific notation blocking
  • Precision exploit prevention
  • Range bounds checking

Data Security

Row Level Security (RLS)

All Supabase tables have RLS enabled. Security tables restrict access to the service role only:

sql
-- Only service role can insert webhook nonces
CREATE POLICY "Service role can insert nonces"
ON webhook_nonces FOR INSERT TO service_role
WITH CHECK (true);

-- Only service role can read security events
CREATE POLICY "Service role can read events"
ON security_events FOR SELECT TO service_role
USING (true);

Security Database Tables

webhook_nonces — Replay Attack Prevention

Correction from Manual Docs

Primary keys use BIGSERIAL (auto-incrementing integer), NOT UUID.

sql
CREATE TABLE webhook_nonces (
  id BIGSERIAL PRIMARY KEY,
  nonce_hash TEXT UNIQUE NOT NULL,
  txn_id TEXT NOT NULL,
  status TEXT,
  amount TEXT,
  order_number TEXT,
  expires_at TIMESTAMPTZ NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

security_events — Audit Logging

sql
CREATE TABLE security_events (
  id BIGSERIAL PRIMARY KEY,
  event_type TEXT NOT NULL,
  severity TEXT NOT NULL,
  source TEXT NOT NULL,
  client_ip TEXT,
  user_email TEXT,
  txn_id TEXT,
  order_number TEXT,
  amount NUMERIC,
  currency TEXT,
  user_agent TEXT,
  status TEXT NOT NULL,
  error_message TEXT,
  event_data JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Database-Level Protections

The payment_transactions table includes PostgreSQL trigger-based protections:

TriggerPurpose
Immutable completedPrevents updates to transactions with completed status — ensures payment records cannot be modified after finalization
Max events limiterLimits the number of payment_transaction_events per transaction to prevent unbounded event accumulation
Cancellation rate limiterRate-limits cancellation operations to prevent abuse

These triggers operate at the database level, providing defense-in-depth protection independent of application code.

Replay Protection Nonce Formula

Two nonce functions are available:

  • Plisio (specific): checkWebhookNonce()SHA256(txn_id:status:amount:order_number) with colon separators
  • Generic (any provider): checkGenericWebhookNonce()SHA256(rawNonceString) where the caller provides the raw string

Correction from Manual Docs

The Plisio nonce formula is SHA256(txn_id:status:amount:order_number) with colon separators — NOT SHA256(txn_id + status + amount + timestamp).

MoonPay uses the generic function with nonce string: SHA256(moonpay_txn_id:status:event_type).

Both functions share the same webhook_nonces table with 5-minute TTL and fail-open behavior.

Security Event Types

The canonical list of event types is defined in app/lib/security-types.ts:

Event TypeSeverityDescription
webhook_receivedinfoWebhook successfully received
hmac_failurecriticalHMAC signature verification failed
replay_detectedcriticalDuplicate webhook nonce detected
rate_limit_violationwarningRate limit exceeded
ip_whitelist_violationcriticalRequest from non-whitelisted IP
payment_successinfoPayment completed successfully
payment_failureerrorPayment processing failed
amount_validation_failedwarningAmount validation rejected
currency_mismatchwarningUnexpected source currency

Security Event Sources

SourceModule
webhook_validatorapp/lib/webhook-validator.server.ts
webhook_processorapp/lib/webhook-processor.server.ts
rate_limiterapp/lib/rate-limiter.server.ts
payment_validatorAmount / currency validation
replay_protectionapp/lib/replay-protection.server.ts
ip_validatorIP whitelist checks
invoice_handlerapp/routes/api.plisio-invoice.ts

API Key Security

KeyExposureUse Case
SUPABASE_ANON_KEYPublic (client)Client-side queries (limited by RLS)
SUPABASE_SERVICE_ROLE_KEYPRIVATEServer-side operations (bypasses RLS)
PLISIO_API_KEYPRIVATEPayment gateway API calls
PLISIO_SECRET_KEYPRIVATEWebhook HMAC verification
MOONPAY_SECRET_KEYPRIVATEMoonPay URL signing
MOONPAY_WEBHOOK_KEYPRIVATEMoonPay webhook verification

Key Storage

EnvironmentStorage
Local.env file (gitignored)
ProductionShopify Oxygen environment variables
CI/CDGitHub Secrets

Monitoring

Critical Event Queries

sql
-- Critical events in last 24 hours
SELECT * FROM security_events
WHERE severity = 'critical'
AND created_at > NOW() - INTERVAL '24 hours'
ORDER BY created_at DESC;

-- Rate limit violations by IP
SELECT client_ip, COUNT(*) as hit_count, MAX(created_at) as last_hit
FROM security_events
WHERE event_type = 'rate_limit_violation'
AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY client_ip
ORDER BY hit_count DESC;

Alerting Thresholds

Set up alerts for:

  • severity = 'critical' events (HMAC failures, replay attacks, IP violations)
  • High volume of rate limit violations from a single IP
  • Multiple payment_failure events in short succession

Compliance Notes

GDPR Data Handling

  • PII Masking: Emails are masked in security logs (e.g., te***[email protected])
  • IP Masking: IPs are partially masked (e.g., 192.168.xxx.xxx)
  • Retention: Security events retained for 365 days
  • Data Purpose: Logged data is used for security monitoring only
  • Deletion: Users can request data deletion

Incident Response

  1. Identify — Check security_events table for anomalies
  2. Contain — Block suspicious IPs via network restrictions
  3. Eradicate — Fix the vulnerability
  4. Recover — Restore normal operation
  5. Learn — Update security measures and documentation

UberLotto Technical Documentation