Deny access to information schema in SQL Server
You should be able to just deny permissions on the entire sys
and information_schema
schema as a whole:
DENY SELECT On SCHEMA::sys To [user_name]
DENY SELECT On SCHEMA::INFORMATION_SCHEMA To [user_name]
That should basically just prevent that user from doing any selects in those two schemas.
Firstly, you are correct in that the (slightly counter-intuitive) way to prevent access to the [sys] and [INFORMATION_SCHEMA] schemas is to first ensure that the login (well, server-level principal) exists as a user (erm, database-level principal) in the master database.
Assume you have a SQL login for simplicity:
CREATE LOGIN [testy] WITH PASSWORD=N'SCoBIqlJELGzrY9zYsKWC5z3kHtMsyCAP6yBHLUYQ0w='
go
Now create a corresponding user in the master database:
use [master]
go
CREATE USER [testy] FOR LOGIN [testy]
go
Now you want to prevent this login from accessing any of the tables in the system-provided schemas - [sys] and [INFORMATION_SCHEMA].
It appears there was a behaviour change between SQL Server 2008 R2 and SQL Server 2012:
In SQL Server 2012 (and presumably later versions), running the following in the [master] database does as you would expect:
DENY SELECT, VIEW DEFINITION ON SCHEMA::[sys] to [testy];
GO
DENY SELECT, VIEW DEFINITION ON SCHEMA::[INFORMATION_SCHEMA] to [testy];
GO
However, in SQL Server 2008 R2 (and presumably earlier versions), the stock grant statements giving access on objects in these schemas to members of [public] seem to override the above DENY statements, which seems like a huge pile of fail to me. Consequently on 2008 R2 you need to explicitly DENY for each GRANT to [public]. Here's a script to do that:
declare
@database_principal sysname,
@cur cursor,
@sql nvarchar( 4000 );
set @database_principal = 'testy';
set @cur = cursor local forward_only static for
select
'DENY ' +
permission_name + ' on ' +
case class
when 1 then
case minor_id
when 0 then 'OBJECT'
else 'COLUMN'
end
else
class_desc
end + '::' +
case class
when 0 then db_name()
when 1 then quotename( OBJECT_SCHEMA_NAME(major_id) ) + '.' + quotename( object_name( major_id ) ) + case minor_id when 0 then '' else ( select '.' + quotename( name ) collate database_default from sys.columns where column_id=minor_id) end
when 3 then schema_name( major_id )
end + ' to ' +
quotename( @database_principal )
from
sys.database_permissions
where
[grantee_principal_id] = 0 -- public
and
[state_desc] = 'GRANT'
and
[permission_name] = 'SELECT'
;
open @cur;
while
1 = 1
begin
fetch @cur into @sql;
if @@fetch_status <> 0 break;
print @sql;
exec sys.sp_executesql @sql;
end;
close @cur;
deallocate @cur;
Run the above in the master database and you've removed access to the contents of those schemas.
Notes:
- Because these are explicit DENY statements, they are correct at the point the script is run. If someone subsequently alters the permissions granted to public (e.g. a service pack creates a new system table) then that will be exposed to the denied user
- It's a good idea to use a database role as the target of the DENY statements and to put the denied users in that role.
- You can undo this by changing the DENY to a REVOKE
If you comment out the following two lines in the above script:
and [permission_name] = 'SELECT'
It will have the effect of undoing ALL of the default GRANTs for public. This will prevent access to e.g., sys.sp_tables and so break e.g. Microsoft Access's ability enumerate the tables at all, but it is useful in high-security scenarios to do just this so the user(s) get access only where you have explicitly granted it.
I am unsure of when this trick became available - since no one has mentioned it - but it appears that it works at least since SQL Server 2008.
DENY VIEW DEFINITION to [database-role / database-user];
The above works without having to add the user to the master
database as mentioned in some of the other answers.