4.8 KiB
Stripe Security Fix - Migration 37
Security Issue Fixed
Issue: The public.active_subscriptions view was defined as a regular view that exposed all users' subscription data. Without proper Row Level Security (RLS) policies, this view could potentially be queried by any authenticated user to see other users' subscription information.
Severity: High - Potential data exposure
Changes Made
Migration 37: sql/37_secure_active_subscriptions.sql
-
Removed insecure view
- Dropped
public.active_subscriptionsview
- Dropped
-
Added secure function
get_my_active_subscription()- Returns only the authenticated user's active subscription- Uses
auth.uid()to filter by current user - Uses
SECURITY DEFINERwith explicitsearch_pathfor security - Granted to
authenticatedrole - Returns fields: subscription_id, user_id, user_email, first_name, last_name, status, current_period_start, current_period_end, cancel_at_period_end, product_name, currency, unit_amount, billing_interval, plan
- Uses
Migration Instructions
1. Run the migration
# Connect to your Supabase database and run:
psql -U postgres -d postgres -f sql/37_secure_active_subscriptions.sql
Or via Supabase Dashboard:
- Go to SQL Editor
- Copy and paste the contents of
sql/37_secure_active_subscriptions.sql - Run the migration
2. Regenerate TypeScript types
After running the migration, regenerate your database types:
# For API
supabase gen types typescript --project-id YOUR_PROJECT_ID > api/src/database.types.ts
# For packages/shared
supabase gen types typescript --project-id YOUR_PROJECT_ID > packages/shared/src/types/database.types.ts
# For xtablo-expo
supabase gen types typescript --project-id YOUR_PROJECT_ID > xtablo-expo/lib/database.types.ts
3. Update any code that references active_subscriptions
If you were querying the view directly (which should be avoided):
// ❌ Old - INSECURE (showed all users' data)
const { data } = await supabase
.from('active_subscriptions')
.select('*');
// ✅ New - SECURE (only shows current user's data)
const { data } = await supabase
.rpc('get_my_active_subscription');
// Returns: {
// subscription_id, user_id, user_email, first_name, last_name,
// status, current_period_start, current_period_end,
// cancel_at_period_end, product_name, currency, unit_amount,
// billing_interval, plan
// }
Current Application Status
✅ Good news: The application code already uses the secure get_user_stripe_subscriptions() function instead of directly querying the view, so no application code changes are needed!
The view was only used for:
- Documentation examples
- Database type definitions (auto-generated)
- Potential ad-hoc queries
Security Best Practices
Why use functions instead of views for sensitive data?
- Explicit access control - Functions can check
auth.uid()to ensure users only see their own data - Permission granularity - Can grant execute permissions to specific roles
- Security definer - Functions run with specific privileges and search paths
- Audit trail - Function calls can be logged more easily than view queries
SECURITY DEFINER best practices
When using SECURITY DEFINER on functions:
-
Always set search_path - Prevents SQL injection via schema manipulation
set search_path = public, stripe -
Always validate inputs - Check
auth.uid()and other user inputswhere (c.metadata->>'user_id')::uuid = auth.uid() -
Minimal permissions - Only grant execute to roles that need it
grant execute on function public.get_my_active_subscription() to authenticated; -
Avoid dynamic SQL - Use parameterized queries, not string concatenation
Testing
Test user access (should work)
-- As an authenticated user, get your own subscription
SELECT * FROM get_my_active_subscription();
-- Should return your subscription or empty result if no active subscription
-- Should only show YOUR data, never other users' data
Test in your application
// In your React component
const { data: subscription } = await supabase
.rpc('get_my_active_subscription');
console.log(subscription);
// Should show your subscription with all fields:
// billing_interval, product_name, status, etc.
Related Files
- Migration:
sql/37_secure_active_subscriptions.sql - Previous migrations:
sql/35_stripe_wrappers.sql(created the view)sql/36_fix_stripe_subscription_dates.sql(updated the view)
- Documentation: This file
Questions?
If you have questions about this security fix, please refer to:
docs/STRIPE_ARCHITECTURE.md- Stripe integration architecturedocs/STRIPE_QUICK_REFERENCE.md- Updated with secure query examples