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:

  1. 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
  2. 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.
  3. You can undo this by changing the DENY to a REVOKE
  4. 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.