Prevent data modification through stored proc - sql-server

In my SQL Server 2005 database, a user belongs to a role and that role has been granted execute permission on all the stored procedures. One of the stored procedures inserts data into the table called tableA.
User has been explicit DENY permission on tableA. However, user can still execute stored procedure to insert new data.
Is there a way to prevent user to insert new data into the table (besides removing user from the role which has execute permission)
create table tableA
(id int identity(1,1), data varchar(20)
)
create proc uspInsertTableA
(#data varchar(20))
with execute as caller
as
begin
insert into tableA
values (#data)
end
DENY INSERT On tableA TO BrianG
GRANT EXECUTE on uspInsertTableA to BrianG
BrianG, can still insert data
exec uspInsertTableA 'yada'

There are two relevant factors:
"execute as" of the stored procedure.
Ownership chaining.
"EXECUTE as CALLER" is already the default (CREATE PROCEDURE ... WITH EXECUTE AS CALLER ...)
Additionally the stored procedure must have a different owner than TableA, to prevent ownership chaining. The owner of the stored procedure must not have the right to insert into the table.
Alternatively you could use the HAS_PERMS_BY_NAME function within the stored procedure to explicitly check whether the user has the permission.
if HAS_PERMS_BY_NAME('TableA','OBJECT','INSERT') = 0
raiserror ('User is not allowed to insert into TableA',16,1)

Could you change the database schema of the sproc? ie. change from dbo.Insert to protected.Insert?
Then you could deny exec against this schema?

Related

SQL Server 2019 cross-database function call permission

I have come across some interesting behaviour in SQL Server 2019 - it does not seem to occur in earlier versions.
If, in database1, I call a function in the same database, which calls a function in database2, which SELECTS a table in database2, I get "The SELECT permission was denied on the object '{TableName}', database '{DbName}', schema 'dbo'."
If, instead, I call the function in database2 directly (without using a function in database1), the query executes successfully.
My question is: what is the logic behind this? I don't understand why I am allowed to read a table in another database, without the SELECT permission, through a function, but not when I call that function using a function in my current database! Is it due to the function preventing the passing on of permissions? I am assuming at the moment that this is an intended change - but I don't understand the logic behind it.
Below is some code demonstrating the behaviour in a simple way.
/*******************************************
SET UP
*******************************************/
CREATE DATABASE TestDb1
GO
CREATE DATABASE TestDb2
GO
CREATE LOGIN [TestLogin] WITH PASSWORD = '123456a.'
GO
--Create users in each database and add to roles.
USE TestDb1
CREATE USER [TestUser] FOR LOGIN [TestLogin]
CREATE ROLE Db1Role
ALTER ROLE Db1Role ADD MEMBER [TestUser]
USE TestDb2
CREATE USER [TestUser] FOR LOGIN [TestLogin]
CREATE ROLE Db2Role
ALTER ROLE Db2Role ADD MEMBER [TestUser]
--Create table in db1, but do no GRANTs on it.
USE TestDb1
CREATE TABLE dbo._testDb1Table (Col1 INT)
GO
--Create a function in db1, and GRANT EXECUTE.
CREATE FUNCTION dbo._TestDb1Function()
RETURNS INT
AS
BEGIN
DECLARE #Result INT = (SELECT TOP (1) Col1 FROM dbo._testDb1Table)
RETURN #Result
END
GO
GRANT EXECUTE ON dbo._TestDb1Function TO Db1Role
GO
--Create a function in db2, and GRANT EXECUTE.
USE TestDb2
GO
CREATE FUNCTION dbo._TestDb2Function()
RETURNS INT
AS
BEGIN
DECLARE #Result INT = (SELECT TestDb1.dbo._TestDb1Function())
RETURN #Result
END
GO
GRANT EXECUTE ON dbo._TestDb2Function TO Db2Role
GO
/*******************************************
TESTS
*******************************************/
USE TestDb2
--Querying TestDb1 by calling the TestDb2 function directly works.
EXECUTE AS LOGIN = 'TestLogin'
SELECT TestDb1.dbo._TestDb1Function()
REVERT
GO
--Querying TestDb2 through a scalar function in db2 doesn't work.
--The SELECT permission was denied on the object '_testDb1Table', database 'TestDb1', schema 'dbo'.
EXECUTE AS LOGIN = 'TestLogin'
SELECT dbo._TestDb2Function()
REVERT
GO
/*******************************************
TIDY UP
*******************************************/
USE [master]
DROP LOGIN [TestLogin]
DROP DATABASE TestDb1
DROP DATABASE TestDb2
This was a bug in SQL Server 2019, caused by scalar UDF inlining.
It was fixed in SQL Server 2019 CU9 (published in February 2021).
For more details see KB4538581.
As per helpful comments by GSerg and Larnu, this behaviour appears to be caused by the scalar UDF inlining feature, added in SQL Server 2019.
It can be fixed by disabling scalar UDF inlining at the database level, in the function definition, or using a query hint.
Edit: as per the answer by Razvan Socol, this has been fixed in SQL Sever 2019 CU9.
Here is the same code as given in the original question, but with these 3 possible solutions inserted into the appropriate places (commented out). Uncommenting any of these 3 solutions allows the script to run without error in SQL Server 2019.
/*******************************************
SET UP
*******************************************/
CREATE DATABASE TestDb1
CREATE DATABASE TestDb2
GO
--SOLUTION 1: Turn off scalar UDF inlining at the database level.
--USE TestDb2
--ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = OFF;
GO
CREATE LOGIN [TestLogin] WITH PASSWORD = '123456a.'
GO
--Create users in each database and add to roles.
USE TestDb1
CREATE USER [TestUser] FOR LOGIN [TestLogin]
CREATE ROLE Db1Role
ALTER ROLE Db1Role ADD MEMBER [TestUser]
USE TestDb2
CREATE USER [TestUser] FOR LOGIN [TestLogin]
CREATE ROLE Db2Role
ALTER ROLE Db2Role ADD MEMBER [TestUser]
--Create table in db1, but do no GRANTs on it.
USE TestDb1
CREATE TABLE dbo._testDb1Table (Col1 INT)
GO
--Create a function in db1, and GRANT EXECUTE.
CREATE FUNCTION dbo._TestDb1Function()
RETURNS INT
AS
BEGIN
DECLARE #Result INT = (SELECT TOP (1) Col1 FROM dbo._testDb1Table)
RETURN #Result
END
GO
GRANT EXECUTE ON dbo._TestDb1Function TO Db1Role
GO
--Create a function in db2, and GRANT EXECUTE.
USE TestDb2
GO
CREATE FUNCTION dbo._TestDb2Function()
RETURNS INT
--SOLUTION 2: Turn off scalar UDF inlining for the function.
--WITH INLINE = OFF
AS
BEGIN
DECLARE #Result INT = (SELECT TestDb1.dbo._TestDb1Function())
RETURN #Result
END
GO
GRANT EXECUTE ON dbo._TestDb2Function TO Db2Role
GO
/*******************************************
TESTS
*******************************************/
USE TestDb2
--Querying TestDb1 by calling the TestDb2 function directly works.
EXECUTE AS LOGIN = 'TestLogin'
SELECT TestDb1.dbo._TestDb1Function()
REVERT
GO
--Querying TestDb2 through a scalar function in db2 doesn't work.
--The SELECT permission was denied on the object '_testDb1Table', database 'TestDb1', schema 'dbo'.
EXECUTE AS LOGIN = 'TestLogin'
SELECT dbo._TestDb2Function()
--SOLUTION 3: Turn off scalar UDF inlining for the query which calls the function.
--OPTION (USE HINT('DISABLE_TSQL_SCALAR_UDF_INLINING')); --Added line
REVERT
GO
/*******************************************
TIDY UP
*******************************************/
USE [master]
DROP LOGIN [TestLogin]
DROP DATABASE TestDb1
DROP DATABASE TestDb2

How to run a schema proc from another schema

Let's say that in a multi-schema db we have these 2 procs:
Create proc S1004.proc1
As
Exec proc2
GO
Create proc S1004.proc2
As
Select 1
Then, when I try to run proc1 from sa login, sql issues an error : Could not find stored procedure 'proc2'.
I know that if we add schema to proc2 in the body of proc1, then it can resolve the schema.
Is there any other solution for this problem.
If you can use your schema in the 'scope' of user (be aware that users and schemas are separated in SQL Server), you could get away with this:
CREATE USER S1004 FOR LOGIN S1004 WITH DEFAULT_SCHEMA = S1004;
GO
CREATE PROCEDURE S1004.proc1
WITH EXECUTE AS 'S1004'
AS
EXECUTE proc2
GO
CREATE PROCEDURE S1004.proc2
AS
SELECT 1
EXEC S1004.proc1
What happens here is that you create user S1004 with a default schema with the same name. That schema will be searched for object when object is not found immediately in the scope of current schema.
When you need to resolve your schema in a procedure, you run the procedure in the context of that user (see WITH EXECUTE AS 'S1004') and schema resolution will succeed.
However, pay attention that this changes execution scope to the another user. It's a workaround, but under the circumstances it's the best you can get.

Grant CREATE TABLE permission only on database but not ALTER

Just trying to play around with permissions, I want to grant CREATE table permission to user but not ALTER or DROP. Moreover user should be able to create table only(no stored proc, no function)
grant alter on schema::dbo to demo_db_user
grant create table to demo_db_user
By using above command, user is able to alter table as well.
You could write a DDL-Trigger and prevent the ALTER TABLE if the current User is not member in a specific role (or whatever), similar like this (untested):
CREATE TRIGGER [ddl_tr_prvent_alter_table] ON DATABASE
FOR alter_procedure
as
begin
if User_Name() <> 'dbo'
RAISERROR ('go ahead, you must not change a table', 16, 1);
end

sql server default procedure permissions

I want to know what are the default permissions for sql server stored procedures.
For example, I created a user in database but no role or no permissions granted except execute permission like:
GRANT EXECUTE ON SCHEMA :: [dbo] TO [newUser]
later on me (as a sa login) created a stored procedure that makes just a select from a table like:
CREATE PROCEDURE dbo.selectX AS
BEGIN
SELECT ID, NAME FROM MyTable
END
and my user [newUser] can execute and see the result of select statement like:
EXEC dbo.selectX
up until now, I can assume that stored procedures have default permissions that my user don't have.
I tried other dmls like UPDATE, INSERT, DELETE and user managed to execute procedures successfully.
But! when I created a proceure with ddl as create table .. such as:
CREATE PROCEDURE dbo.crtNT AS
BEGIN
CREATE TABLE del(id int) --for test
END
and when the newUser executes this procedure error occurs like:
CREATE TABLE permission denied in database 'MyDb'
And here is my question: By default, do stored procedures have permission with DML(select, insert, update vs.) but not with DDL(create, drop table vs.)? Because I didn't see any explanation about this situation in Books Online or anywhere else.
Any comments will be welcomed.
You are correct in your assumption that DDL statements are treated differently.
The procedures with DML statements work through the mechanism of Ownership Chaining however as this quote from the EXECUTE AS documentation states
Remember that ownership chaining applies only to DML statements.

Stored Procedure and Permissions - Is EXECUTE enough?

I have a SQL Server 2008 database where all access to the underlying tables is done through stored procedures. Some stored procedures simply SELECT records from the tables while others UPDATE, INSERT, and DELETE.
If a stored procedure UPDATES a table does the user executing the stored procedure also need UPDATE permissions to the affected tables or is the fact that they have EXECUTE permissions to the stored procedure enough?
Basically I am wondering if giving the user EXECUTE permissions to the stored procedures is enough or do I need to give them SELECT, UPDATE, DELETE, and INSERT permissions to the tables in order for the stored procedures to work. Thank you.
[EDIT] In most of my stored procedures it does indeed appear that EXECUTE is enough. However, I did find that in stored procedures where "Execute sp_Executesql" was used that EXECUTE was not enough. The tables involved needed to have permissions for the actions being performed within "sp_Executesql".
Permissions on tables are not checked (including DENY) if tables and proc have the same owner. They can be in different schemas too as long as the schemas have the same owner.
See Ownership chaining on MSDN
Edit, from a comment from a deleted answer.
The context is always the current login unless EXECUTE AS as been used: only referenced object DML permissions are not checked. Try OBJECT_ID(referencedtable) in a stored proc where no rights are assigned to referencedtable. It gives NULL. If executed by the owner of the stored proc then it would give a value because owener has rights on referencedtable
Execute permissions on the stored procedure is sufficient.
CREATE TABLE dbo.Temp(n int)
GO
DENY INSERT ON dbo.Temp TO <your role>
GO
CREATE PROCEDURE dbo.SPTemp(#Int int)
AS
INSERT dbo.Temp
SELECT #Int
GO
GRANT EXEC ON dbo.SPTemp TO <your role>
GO
Then the (non-db_owner) user will have the following rights:
EXEC dbo.SPTemp 10
GO
INSERT dbo.Temp --INSERT permission was denied on the object 'Temp'
SELECT 10
However, if there is dynamic SQL inside dbo.SPTemp that attempts to insert into dbo.Temp then that will fail. In this case direct permission on the table will need to be granted.
Maybe you can use
"with execute as owner"
when you create the stored procedure, such as below:
create procedure XXX
with execute as owner
as
begin
...
end
go
Then you only need to grant the user the EXECUTE permission for the stored procedure XXX.
Execute permission on a stored procedure that does an insert, update, or delete is sufficient. You do not need to grant those permissions at the table level. In fact, I would discourage that approach. Using a stored procedure gives you more control over how the change occurs. For instance, you may wish to do some checking prior to allowing the update. Using a stored procedure can also help prevent major accidents--like deleting all the rows in the table because someone forgot the WHERE clause!
THANK YOU SO MUCH! I had a similar problem. This lead me to the answer.
I was attempting to trunctate a table in a stored procedure that called other stored procedures that were nested in IF statements.
My error was
The server principal "domain\my_id" is not able to access the database "2nd_DB" under the current security context.
I had given the calling stored procedure rights to do the truncate (EXECUTE AS SELF), which then caused a problem because SELF didn't have rights to the 2nd DB. Our solution was to move the truncate to another SP, include the EXECUTE AS SELF. We now call the truncate SP, execute our data processing, make logic determination, and call the appropriate 3rd SP.

Resources