-- ════════════════════════════════════════════════════════════════ -- SECURITY HARDENING: Stored Procedure Ownership Enforcement -- ════════════════════════════════════════════════════════════════ -- -- PURPOSE: Add WHERE clientId checks to all stored procedures -- that accept initiativeId, channelCampaignId, or wizardId. -- -- The Gateway now passes clientId in all JSON requests. -- These proc changes enforce ownership at the database level -- as a SECOND layer of defense (the Gateway guard is the first). -- -- APPLY: Run against your AdPlatform SQL Server database. -- TEST FIRST in dev/staging before production. -- ════════════════════════════════════════════════════════════════ -- ────────────────────────────────────────────────── -- PATTERN: Inside each proc's @Action handler, add: -- -- DECLARE @clientId NVARCHAR(100) = JSON_VALUE(@Rqst, '$.clientId'); -- -- -- Then in every SELECT/UPDATE/DELETE that references an initiative: -- WHERE i.initiativeId = @initiativeId -- AND i.clientId = @clientId -- ← ADD THIS -- -- -- If the WHERE filters out the row, return "not found" -- -- (same response as non-existent ID — prevents enumeration) -- ────────────────────────────────────────────────── PRINT '=== Security Hardening Migration ===' PRINT '' -- ────────────────────────────────────────────────── -- 1. spInitiative — get, update, updateStatus, delete -- ────────────────────────────────────────────────── PRINT 'Hardening spInitiative...' -- Example pattern for the "get" action: -- (Apply this pattern to get, update, updateStatus, delete actions) -- -- Current: -- SELECT ... FROM tbInitiative WHERE initiativeId = @initiativeId -- -- Hardened: -- DECLARE @clientId NVARCHAR(100) = JSON_VALUE(@Rqst, '$.clientId') -- SELECT ... FROM tbInitiative -- WHERE initiativeId = @initiativeId -- AND (@clientId IS NULL OR clientId = @clientId) -- -- The @clientId IS NULL fallback allows internal/system calls -- (like InitiativeLaunchService) that don't pass clientId to still work. -- IMPORTANT: Apply to each action in spInitiative: -- 'get' → WHERE initiativeId = @id AND (@clientId IS NULL OR clientId = @clientId) -- 'update' → same -- 'updateStatus' → same -- 'delete' → same -- 'list' → already scoped by clientId (verify it uses = not LIKE) GO -- ────────────────────────────────────────────────── -- 2. spChannelCampaign — get, sync -- ────────────────────────────────────────────────── PRINT 'Hardening spChannelCampaign...' -- Channel campaigns link to initiatives, so ownership requires a JOIN: -- -- Current: -- SELECT cc.* FROM tbChannelCampaign cc WHERE cc.channelCampaignId = @id -- -- Hardened: -- SELECT cc.* -- FROM tbChannelCampaign cc -- JOIN tbInitiative i ON cc.initiativeId = i.initiativeId -- WHERE cc.channelCampaignId = @id -- AND (@clientId IS NULL OR i.clientId = @clientId) -- -- For 'sync' action: This is now admin-only in the Gateway, -- but add the JOIN anyway for defense in depth. GO -- ────────────────────────────────────────────────── -- 3. spCampaignWizard — get, updateStep, setStep, submit, updateStatus, delete -- ────────────────────────────────────────────────── PRINT 'Hardening spCampaignWizard...' -- Current: -- SELECT ... FROM tbCampaignWizard WHERE wizardId = @wizardId -- -- Hardened: -- DECLARE @clientId NVARCHAR(100) = JSON_VALUE(@Rqst, '$.clientId') -- SELECT ... FROM tbCampaignWizard -- WHERE wizardId = @wizardId -- AND (@clientId IS NULL OR clientId = @clientId) GO -- ────────────────────────────────────────────────── -- 4. spAllocation — all actions -- ────────────────────────────────────────────────── PRINT 'Hardening spAllocation...' -- Allocations link to initiatives: -- -- Hardened: -- JOIN tbInitiative i ON a.initiativeId = i.initiativeId -- WHERE a.initiativeId = @initiativeId -- AND (@clientId IS NULL OR i.clientId = @clientId) GO -- ────────────────────────────────────────────────── -- 5. Status transition validation at DB level (optional extra layer) -- ────────────────────────────────────────────────── PRINT 'Adding status transition function...' -- Create a function the procs can call to validate transitions: IF OBJECT_ID('dbo.fnIsValidStatusTransition', 'FN') IS NOT NULL DROP FUNCTION dbo.fnIsValidStatusTransition GO CREATE FUNCTION dbo.fnIsValidStatusTransition( @currentStatus VARCHAR(20), @requestedStatus VARCHAR(20), @isSystem BIT = 0 -- 1 = system/admin (broader transitions allowed) ) RETURNS BIT AS BEGIN -- System can do anything IF @isSystem = 1 RETURN 1 -- Client-allowed transitions IF @currentStatus = 'active' AND @requestedStatus = 'paused' RETURN 1 IF @currentStatus = 'paused' AND @requestedStatus = 'active' RETURN 1 IF @currentStatus IN ('draft','staged','pending','active','paused') AND @requestedStatus = 'cancelled' RETURN 1 RETURN 0 END GO -- ────────────────────────────────────────────────── -- 6. spGoogleAccount — validate -- ────────────────────────────────────────────────── PRINT 'Verifying spGoogleAccount...' -- The validate action should verify that the customerId -- belongs to the requesting client. Current implementation -- may not check this — verify and add: -- -- WHERE a.customerId = @customerId -- AND a.clientId = @clientId GO PRINT '' PRINT '=== Migration complete ===' PRINT 'NOTE: This is a TEMPLATE. Review each stored procedure and apply' PRINT 'the ownership WHERE clauses to match your exact table/column names.' PRINT '' PRINT 'After applying, test:' PRINT ' 1. Normal user can only see their own initiatives/wizards' PRINT ' 2. User A cannot access User B resources by guessing IDs' PRINT ' 3. LaunchService (no clientId) can still read initiatives' PRINT ' 4. Admin role can sync channel status' GO