/* ---- Proc Inserts via Dynamic SQL into Table with Trigger that Inserts into Other Table ---- Created By: Solomon Rutzky / Sql Quantum Leap Created On: 2019-05-10 Updated On: n/a Location: https://pastebin.com/ALgLuZAP Related blog post: https://SqlQuantumLeap.com/2017/12/30/please-please-please-stop-using-impersonation-execute-as/ --------------------------------------------------------------------------------------------- */ -- ========================================================================= -- ==== ==== -- ==== This script is a step-by-step walk-through of a common ==== -- ==== scenario in which additional permissions are needed due to ==== -- ==== the use of Dynamic SQL. But, instead of using EXECUTE AS ==== -- ==== (i.e. impersonation), we will use Module Signing to get only ==== -- ==== one or two additional permissions. As you will see, setting ==== -- ==== up the Certificate is fairly easy to do. ==== -- ==== ==== -- ==== For more info, please visit: https://ModuleSigning.Info/ ==== -- ==== ==== -- ========================================================================= GO -- https://SqlQuantumLeap.com/2018/12/28/prevent-full-script-execution-understanding-and-using-parseonly-and-noexec/ SET PARSEONLY ON; GO ---------------------------------------------------------------- -- SETUP ---------------------------------------------------------------- USE [tempdb]; -- Initial target table: IF (OBJECT_ID(N'dbo.Step2') IS NULL) BEGIN PRINT 'Creating table [Step2]...'; CREATE TABLE dbo.Step2 ( [Step2ID] INT NOT NULL IDENTITY(1, 1) CONSTRAINT [PK_Step2] PRIMARY KEY, [SomeValue] VARCHAR(50) NOT NULL, [InsertTime] DATETIME NOT NULL CONSTRAINT [DF_Step2_InsertTime] DEFAULT (GETDATE()) ); END; -- Destination / audit table: IF (OBJECT_ID(N'dbo.Step4') IS NULL) BEGIN PRINT 'Creating table [Step4]...'; CREATE TABLE dbo.Step4 ( [Step4ID] INT NOT NULL IDENTITY(1, 1) CONSTRAINT [PK_Step4] PRIMARY KEY, [TableName] sysname, [Operation] CHAR(1) NOT NULL, [ID] INT NOT NULL, [NewValue] VARCHAR(50) NULL, [InsertTime] DATETIME NOT NULL CONSTRAINT [DF_Step4_InsertTime] DEFAULT (GETDATE()) ); END; -- Audit Trigger on dbo.Step2 that inserts into dbo.Step4: IF (OBJECT_ID(N'dbo.Step3') IS NULL) BEGIN PRINT 'Creating trigger [Step3]...'; -- DROP TRIGGER dbo.[Step3]; EXEC (N' CREATE TRIGGER dbo.Step3 ON dbo.Step2 AFTER INSERT, UPDATE, DELETE AS SET NOCOUNT ON; IF (EXISTS(SELECT * FROM inserted)) BEGIN IF (EXISTS(SELECT * FROM inserted WHERE [SomeValue] = ''dynamic'')) BEGIN -- Dynamic SQL cannot see "inserted" pseudo-table SELECT * INTO #Inserted FROM inserted; -- Break ownership-chain (again!) EXEC sp_executesql N''INSERT INTO dbo.Step4 ([TableName], [Operation], [ID], [NewValue]) SELECT @TableName_tmp, @Operation_tmp, ins.[Step2ID], ins.[SomeValue] FROM #Inserted ins;'', N''@TableName_tmp sysname, @Operation_tmp CHAR(1)'', @TableName_tmp = N''dbo.Step2'', @Operation_tmp = ''I''; END; ELSE BEGIN INSERT INTO dbo.Step4 ([TableName], [Operation], [ID], [NewValue]) SELECT N''dbo.Step2'', ''I'', ins.[Step2ID], ins.[SomeValue] FROM inserted ins; END; END; '); END; -- Stored Procedure that inserts into dbo.Step2 (in Dynamic SQL): IF (OBJECT_ID(N'dbo.Step1') IS NULL) BEGIN PRINT 'Creating stored procedure [Step1]...'; EXEC (N' CREATE PROCEDURE dbo.Step1 ( @SomeValue VARCHAR(50), @ExtraRows TINYINT = 0 -- test set-based inserts ) AS SET NOCOUNT ON; -- Dynamic SQL breaks ownership chain: EXEC sp_executesql N''INSERT INTO dbo.Step2 ([SomeValue]) VALUES (@SomeValue_tmp);'', N''@SomeValue_tmp VARCHAR(50)'', @SomeValue_tmp = @SomeValue; IF (@ExtraRows > 0) BEGIN EXEC sp_executesql N''INSERT INTO dbo.Step2 ([SomeValue]) SELECT TOP (@ExtraRows_tmp) CONVERT(VARCHAR(50), NEWID()) FROM master.sys.columns;'', N''@ExtraRows_tmp TINYINT'', @ExtraRows_tmp = @ExtraRows; END; '); END; -- Test Login: IF (SUSER_ID(N'PermissionsTestLogin') IS NULL) BEGIN PRINT 'Creating login [PermissionsTestLogin]...'; CREATE LOGIN [PermissionsTestLogin] WITH PASSWORD = 'A So-So Password 1234'; END; -- Test User: IF (DATABASE_PRINCIPAL_ID(N'PermissionsTestUser') IS NULL) BEGIN PRINT 'Creating user [PermissionsTestUser]...'; CREATE USER [PermissionsTestUser] FOR LOGIN [PermissionsTestLogin]; END; GO ---------------------------------------------------------------- -- TEST 1: ensure no extra security results in an error ---------------------------------------------------------------- EXECUTE AS LOGIN = N'PermissionsTestLogin'; SELECT ORIGINAL_LOGIN() AS [OriginalLogin], SUSER_NAME() AS [CurrentLogin], SESSION_USER AS [SessionUser], DATABASE_PRINCIPAL_ID() AS [DBPrincipalID]; -- OriginalLogin CurrentLogin SessionUser DBPrincipalID -- ALBRIGHT\Solomon PermissionsTestLogin PermissionsTestUser 5 INSERT INTO dbo.Step2 ([SomeValue]) VALUES ('should error'); /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step2', database 'tempdb', schema 'dbo'. */ INSERT INTO dbo.Step4 ([TableName], [Operation], [ID], [NewValue]) VALUES ('should error', 'e', -1, 'error'); /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step4', database 'tempdb', schema 'dbo'. */ EXEC dbo.Step1 'this should error'; /* Msg 229, Level 14, State 5, Procedure dbo.Step1, Line XXXXX [Batch Start Line YYYYY] The EXECUTE permission was denied on the object 'Step1', database 'tempdb', schema 'dbo'. */ REVERT; SELECT SESSION_USER AS [SessionUser]; -- dbo GO ---------------------------------------------------------------- -- TEST 2: grant permission to the stored procedure ---------------------------------------------------------------- GRANT EXECUTE ON dbo.Step1 TO [PermissionsTestUser]; EXECUTE AS LOGIN = N'PermissionsTestLogin'; SELECT ORIGINAL_LOGIN() AS [OriginalLogin], SUSER_NAME() AS [CurrentLogin], SESSION_USER AS [SessionUser], DATABASE_PRINCIPAL_ID() AS [DBPrincipalID]; -- OriginalLogin CurrentLogin SessionUser DBPrincipalID -- ALBRIGHT\Solomon PermissionsTestLogin PermissionsTestUser 5 -- Same outcome as before: INSERT INTO dbo.Step2 ([SomeValue]) VALUES ('should error'); /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step2', database 'tempdb', schema 'dbo'. */ -- Same outcome as before: INSERT INTO dbo.Step4 ([TableName], [Operation], [ID], [NewValue]) VALUES ('should error', 'e', -1, 'error'); /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step4', database 'tempdb', schema 'dbo'. */ -- DIFFERENT: EXEC dbo.Step1 'this should also error'; /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step2', database 'tempdb', schema 'dbo'. */ REVERT; SELECT SESSION_USER AS [SessionUser]; -- dbo GO ---------------------------------------------------------------- -- TEST 3: use module signing to allow proc to insert into dbo.Step2 ---------------------------------------------------------------- ------------------------------ IF (CERT_ID(N'PermissionsTestCert') IS NULL) BEGIN PRINT 'Creating certificate [PermissionsTestCert]...'; CREATE CERTIFICATE [PermissionsTestCert] ENCRYPTION BY PASSWORD = 'simple cert password, y0!' WITH SUBJECT = 'avoid impersonation / TRUSTWORTHY ON / cross-DB ownership chaining', EXPIRY_DATE = '2099-10-31'; END; IF (DATABASE_PRINCIPAL_ID(N'PermissionsTestCertUser') IS NULL) BEGIN PRINT 'Creating user [PermissionsTestCertUser]...'; CREATE USER [PermissionsTestCertUser] FROM CERTIFICATE [PermissionsTestCert]; END; GRANT INSERT ON dbo.Step2 TO [PermissionsTestCertUser]; -- Associate the insert permission (given to [PermissionsTestCertUser]) with the stored procedure: IF (NOT EXISTS( SELECT * FROM sys.crypt_properties sig INNER JOIN sys.certificates crt ON crt.[thumbprint] = sig.[thumbprint] WHERE sig.[major_id] = OBJECT_ID(N'dbo.Step1') AND crt.[name] = N'PermissionsTestCert' )) BEGIN PRINT 'Adding signature to [Step1]...'; ADD SIGNATURE TO dbo.[Step1] BY CERTIFICATE [PermissionsTestCert] WITH PASSWORD = 'simple cert password, y0!'; END; ------------------------------ EXECUTE AS LOGIN = N'PermissionsTestLogin'; SELECT ORIGINAL_LOGIN() AS [OriginalLogin], SUSER_NAME() AS [CurrentLogin], SESSION_USER AS [SessionUser], DATABASE_PRINCIPAL_ID() AS [DBPrincipalID]; -- OriginalLogin CurrentLogin SessionUser DBPrincipalID -- ALBRIGHT\Solomon PermissionsTestLogin PermissionsTestUser 5 -- Same outcome as before: INSERT INTO dbo.Step2 ([SomeValue]) VALUES ('should error'); /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step2', database 'tempdb', schema 'dbo'. */ -- Same outcome as before: INSERT INTO dbo.Step4 ([TableName], [Operation], [ID], [NewValue]) VALUES ('should error', 'e', -1, 'error'); /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step4', database 'tempdb', schema 'dbo'. */ -- DIFFERENT: EXEC dbo.Step1 'this should work'; -- SUCCESS!! -- DIFFERENT: EXEC dbo.Step1 'this should also work', 3; -- SUCCESS!! SELECT * FROM dbo.[Step2]; /* Msg 229, Level 14, State 5, Line XXXXX The SELECT permission was denied on the object 'Step2', database 'tempdb', schema 'dbo'. */ SELECT * FROM dbo.[Step4]; /* Msg 229, Level 14, State 5, Line XXXXX The SELECT permission was denied on the object 'Step4', database 'tempdb', schema 'dbo'. */ REVERT; SELECT SESSION_USER AS [SessionUser]; -- dbo SELECT * FROM dbo.[Step2]; -- 6 rows SELECT * FROM dbo.[Step4]; -- 6 rows -- ========================================================================= -- ==== CONCLUSION #1 ==== -- ==== ==== -- ==== Bare-minimum requirements: ==== -- ==== 1) Sign Stored Procedure ==== -- ==== 2) GRANT signature-based User INSERT on main Table ==== -- ==== ==== -- ==== IF Trigger uses static SQL then no additional requirements as ==== -- ==== Ownership-Chaining implicitly grants DML permissions on ==== -- ==== objects owned by the same database principal that owns ==== -- ==== the Trigger. ==== -- ==== ==== -- ==== IF Trigger uses Dynamic SQL then additional steps are needed ==== -- ==== as Dynamic SQL breaks Ownership-Chaining. Proceed with ==== -- ==== following tests for details. ==== -- ==== ==== -- ========================================================================= GO ---------------------------------------------------------------- -- TEST 4: check extent / scope of signature-based permissions ---------------------------------------------------------------- EXECUTE AS LOGIN = N'PermissionsTestLogin'; SELECT ORIGINAL_LOGIN() AS [OriginalLogin], SUSER_NAME() AS [CurrentLogin], SESSION_USER AS [SessionUser], DATABASE_PRINCIPAL_ID() AS [DBPrincipalID]; -- OriginalLogin CurrentLogin SessionUser DBPrincipalID -- ALBRIGHT\Solomon PermissionsTestLogin PermissionsTestUser 5 -- DIFFERENT: EXEC dbo.Step1 'dynamic'; /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step4', database 'tempdb', schema 'dbo'. */ REVERT; SELECT SESSION_USER AS [SessionUser]; -- dbo SELECT * FROM dbo.[Step2]; -- 6 rows SELECT * FROM dbo.[Step4]; -- 6 rows GO ---------------------------------------------------------------- -- TEST 5: grant permission to certificate ---------------------------------------------------------------- ------------------------------ GRANT INSERT ON dbo.Step4 TO [PermissionsTestCertUser]; ------------------------------ EXECUTE AS LOGIN = N'PermissionsTestLogin'; SELECT ORIGINAL_LOGIN() AS [OriginalLogin], SUSER_NAME() AS [CurrentLogin], SESSION_USER AS [SessionUser], DATABASE_PRINCIPAL_ID() AS [DBPrincipalID]; -- OriginalLogin CurrentLogin SessionUser DBPrincipalID -- ALBRIGHT\Solomon PermissionsTestLogin PermissionsTestUser 5 -- DIFFERENT: EXEC dbo.Step1 'dynamic'; /* Msg 229, Level 14, State 5, Line XXXXX The INSERT permission was denied on the object 'Step4', database 'tempdb', schema 'dbo'. */ REVERT; SELECT SESSION_USER AS [SessionUser]; -- dbo SELECT * FROM dbo.[Step2]; -- 6 rows SELECT * FROM dbo.[Step4]; -- 6 rows GO ---------------------------------------------------------------- -- TEST 6: add signature to trigger to handle Dynamic SQL ---------------------------------------------------------------- ------------------------------ -- Associate the insert permission (given to [PermissionsTestCertUser]) with the trigger: IF (NOT EXISTS( SELECT * FROM sys.crypt_properties sig INNER JOIN sys.certificates crt ON crt.[thumbprint] = sig.[thumbprint] WHERE sig.[major_id] = OBJECT_ID(N'dbo.Step3') AND crt.[name] = N'PermissionsTestCert' )) BEGIN PRINT 'Adding signature to [Step3]...'; ADD SIGNATURE TO dbo.[Step3] BY CERTIFICATE [PermissionsTestCert] WITH PASSWORD = 'simple cert password, y0!'; END; ------------------------------ EXECUTE AS LOGIN = N'PermissionsTestLogin'; SELECT ORIGINAL_LOGIN() AS [OriginalLogin], SUSER_NAME() AS [CurrentLogin], SESSION_USER AS [SessionUser], DATABASE_PRINCIPAL_ID() AS [DBPrincipalID]; -- OriginalLogin CurrentLogin SessionUser DBPrincipalID -- ALBRIGHT\Solomon PermissionsTestLogin PermissionsTestUser 5 -- DIFFERENT: EXEC dbo.Step1 'dynamic'; -- SUCCESS!! REVERT; SELECT SESSION_USER AS [SessionUser]; -- dbo SELECT * FROM dbo.[Step2]; -- 7 rows SELECT * FROM dbo.[Step4]; -- 7 rows -- ========================================================================= -- ==== CONCLUSION #2 ==== -- ==== ==== -- ==== Additional requirements (ONLY IF Trigger uses Dynamic SQL): ==== -- ==== 3) Sign Trigger ==== -- ==== 4) GRANT signature-based User INSERT on secondary Table ==== -- ==== ==== -- ========================================================================= GO ---------------------------------------------------------------- -- CLEAN UP ---------------------------------------------------------------- /* -- Highlight to execute: DROP PROCEDURE dbo.Step1; DROP TABLE dbo.Step2; DROP TABLE dbo.Step4; DROP USER [PermissionsTestCertUser]; DROP CERTIFICATE [PermissionsTestCert]; */ GO SET PARSEONLY OFF; RAISERROR(' This script is not meant to run all at once. Please highlight each section individually to execute.', 16, 1); GO