3.7 KiB
3.7 KiB
Purchase Lead Security Fix
Issue Summary
The purchase_lead function had a potential security vulnerability where the frontend was passing a client-controlled creditsCost parameter, which could theoretically allow providers to purchase high-value leads for arbitrary low prices.
Root Cause
- Migration 00010: Created
purchase_lead(UUID, UUID, INTEGER)with client-controlled price parameter - Migration 00012: Created safe 2-arg version
purchase_lead(UUID, UUID)that gets price from server - Problem: Frontend code still called the function with 3 arguments, passing
p_credits_cost
Security Fix Applied
1. Database Migration (00058)
Created migration drop_insecure_purchase_lead_overload that:
- Explicitly drops any 3-argument version of
purchase_leadif it exists - Adds documentation comment to the safe 2-arg function
- Ensures only the secure version remains in the database
2. Frontend API Fix
File: src/db/api.ts
- Removed
creditsCostparameter fromleadPurchasesApi.purchase()function signature - Removed
p_credits_costfrom the RPC call parameters - Added comment documenting that price is determined server-side
Before:
async purchase(providerId: string, leadId: string, creditsCost: number = 10) {
const { data, error } = await supabase.rpc('purchase_lead', {
p_provider_id: providerId,
p_lead_id: leadId,
p_credits_cost: creditsCost, // ❌ Client-controlled
});
}
After:
async purchase(providerId: string, leadId: string) {
const { data, error } = await supabase.rpc('purchase_lead', {
p_provider_id: providerId,
p_lead_id: leadId, // ✅ Price from server
});
}
3. Component Update
File: src/components/provider/LeadDetailModal.tsx
- Updated call to
leadPurchasesApi.purchase()to only pass 2 arguments - Added comment documenting server-side price determination
How the Secure Function Works
The safe purchase_lead(UUID, UUID) function:
-
Gets price from database:
SELECT final_price INTO v_lead_price FROM leads WHERE id = p_lead_id; -
Uses calculated/override price:
final_price=override_price(if set by admin) ORcalculated_pricecalculated_priceis computed based on activities (balloon, ATV, tours, etc.)- Base price is 20 credits, with multipliers for premium activities
-
Atomic transaction:
- Locks wallet row with
FOR UPDATE - Verifies sufficient balance
- Deducts correct amount
- Records purchase with actual price paid
- Creates transaction record
- Locks wallet row with
Verification
-- Only the safe 2-arg function exists
SELECT
proname,
pg_get_function_arguments(oid) as args
FROM pg_proc
WHERE proname = 'purchase_lead';
-- Result:
-- purchase_lead | p_provider_id uuid, p_lead_id uuid
Impact
- ✅ Security: Clients can no longer specify arbitrary prices
- ✅ Integrity: All purchases use server-calculated pricing
- ✅ Audit:
lead_purchases.price_paidaccurately reflects actual cost - ✅ Consistency: Frontend and backend are now aligned
Testing Recommendations
- Verify lead purchase flow works correctly
- Confirm correct price is deducted from wallet
- Check that high-value leads (with balloon, ATV, etc.) charge appropriate amounts
- Verify admin price overrides work correctly
- Test insufficient balance scenarios
Related Files
supabase/migrations/00010_create_purchase_lead_function.sql(original vulnerable version)supabase/migrations/00012_add_lead_pricing_system.sql(safe version created)supabase/migrations/00058_drop_insecure_purchase_lead_overload.sql(cleanup migration)src/db/api.ts(API layer)src/components/provider/LeadDetailModal.tsx(UI component)