Unlike stored procedures, triggers, UDFs, and other
types of code modules that can be exposed within SQL Server, a given
SQLCLR routine is not directly related to a database, but rather to an
assembly cataloged within the database. Cataloging of an assembly is done using SQL Server's CREATE ASSEMBLY
statement, and unlike their T-SQL equivalents, SQLCLR modules get their
first security restrictions not via grants, but rather at the same time
their assemblies are cataloged. The CREATE ASSEMBLY statement allows the DBA or database developer to specify one of three security and reliability permission sets that dictate what the code in the assembly is allowed to do.
The allowed permission sets are SAFE, EXTERNAL_ACCESS, and UNSAFE. Permissions granted by each set are nested to include the lower sets' permissions. The set of permissions allowed for SAFE
assemblies includes limited access to math and string functions, along
with data access to the host database via the context connection. The EXTERNAL_ACCESS
permission set adds the ability to communicate outside of the SQL
Server instance, to other database servers, file servers, web servers,
and so on. And the UNSAFE permission set gives the assembly the ability to do pretty much anything—including running unmanaged code.
Although exposed as
only a single user-controllable setting, internally each permission
set's rights are actually enforced by two distinct methods. Assemblies
assigned to each permission set are granted access to do certain
operations via .NET's Code Access Security (CAS) technology. At the same time, access is limited to certain operations based on checks against a.NET 2.0 attribute called HostProtectionAttribute
(HPA). On the surface, the difference between HPA and CAS is that they
are opposites: CAS permissions dictate what an assembly can do, whereas
HPA permissions dictate what an assembly cannot do. The combination of
everything granted by CAS and everything denied by HPA makes up each of
the three permission sets.
Beyond this basic difference is
a much more important differentiation between the two access control
methods. Although violation of a permission enforced by either method
will result in a runtime exception, the actual checks are done at very
different times. CAS grants are checked dynamically at run time via a
stack walk done as code is executed. On the other hand, HPA permissions
are checked at the point of just-in-time compilation—just before calling the method being referenced.
To observe how
these differences affect the way code runs, a few test cases will be
necessary. To begin with, let's take a look at how a CAS exception
works. Create a new assembly containing the following CLR stored
procedure:
[SqlProcedure]
public static void CAS_Exception()
{
SqlContext.Pipe.Send("Starting...");
using (System.IO.FileStream fs =
new FileStream(@"c:\b.txt", FileMode.Open))
{
//Do nothing...
}
SqlContext.Pipe.Send("Finished...");
return;
}
Catalog the assembly as SAFE and execute the stored procedure. This will result in the following output:
Starting...
Msg 6522, Level 16, State 1, Procedure CAS_Exception, Line 0
A .NET Framework error occurred during execution of user-defined routine or
aggregate "CAS_Exception":
System.Security.SecurityException: Request for the permission of type
'System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
System.Security.SecurityException:
at System.Security.CodeAccessSecurityEngine.Check(Object demand,
StackCrawlMark& stackMark, Boolean isPermSet)
at System.Security.CodeAccessPermission.Demand()
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32
rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options,
SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy)
at System.IO.FileStream..ctor(String path, FileMode mode)
at udf_part2.CAS_Exception()
.
The exception thrown in this case is a SecurityException, indicating that this was a CAS violation (of the FileIOPermission
type). But the exception is not the only thing that happened; notice
that the first line of the output is the string "Starting...", which was
output by the SqlPipe.Send method
used in the first line of the stored procedure. So before the exception
was hit, the method was entered and code execution succeeded until the
actual permissions violation was attempted.
NOTE
File I/O is a good
example of access to a resource—local or otherwise—that is outside of
what the context connection allows. Avoiding this particular violation
using the SQLCLR security buckets would require cataloging the assembly
using the EXTERNAL_ACCESS permission.
To see how HPA
exceptions behave, let's try the same experiment again, this time with
the following stored procedure (again, cataloged as SAFE):
[SqlProcedure]
public static void HPA_Exception()
{
SqlContext.Pipe.Send("Starting...");
//The next line will throw an HPA exception...
Monitor.Enter(SqlContext.Pipe);
//Release the lock (if the code even gets here)...
Monitor.Exit(SqlContext.Pipe);
SqlContext.Pipe.Send("Finished...");
return;
}
Just like before, an exception occurs. But this time, the output is a bit different:
Msg 6522, Level 16, State 1, Procedure HPA_Exception, Line 0
A .NET Framework error occurred during execution of user-defined routine or
aggregate "HPA_Exception":
System.Security.HostProtectionException: Attempted to perform an operation that was
forbidden by the CLR host.
The protected resources (only available with full trust) were: All
The demanded resources were: Synchronization, ExternalThreading
System.Security.HostProtectionException:
at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(Assembly asm,
PermissionSet granted, PermissionSet refused, RuntimeMethodHandle rmh,
SecurityAction action, Object demand, IPermission permThatFailed)
at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(Object
assemblyOrString, PermissionSet granted, PermissionSet refused, RuntimeMethodHandle
rmh, SecurityAction action, Object demand, IPermission permThatFailed)
at System.Security.CodeAccessSecurityEngine.CheckSetHelper(PermissionSet grants,
PermissionSet refused, PermissionSet demands, RuntimeMethodHandle rmh, Object
assemblyOrString, SecurityAction action, Boolean throwException)
at System.Security.CodeAccessSecurityEngine.CheckSetHelper(CompressedStack cs,
PermissionSet grants, PermissionSet refused, PermissionSet demands,
RuntimeMethodHandle rmh, Assembly asm, SecurityAction action)
at udf_part2.HPA_Exception()
.
Unlike when executing the CAS_Exception stored procedure, this time we do not see the "Starting..." message, indicating that the SqlPipe.Send method was not called before hitting the exception. As a matter of fact, the HPA_Exception
method was not ever entered at all during the code execution phase. You
can verify this by attempting to set a breakpoint inside of the
function and starting a debug session in Visual Studio. The reason that
the breakpoint can't be hit is that the permissions check was done, and
the exception thrown, immediately after just-in-time compilation.
You should also note that
the wording of the exception is quite a bit different in this case. The
wording of the CAS exception is a rather benign "Request for the
permission ... failed." On the other hand, the HPA exception carries a
much sterner warning: "Attempted to perform an operation that was forbidden."
This difference in wording is not accidental. CAS grants are concerned
with security—keep code from being able to access something protected
because it's not supposed to have access. HPA permissions, on the other
hand, are concerned with server reliability and keeping the CLR host
running smoothly and efficiently. Threading and synchronization are
considered potentially threatening to reliability and are therefore
limited to assemblies marked as UNSAFE.
NOTE
Using Reflector or
another .NET disassembler, it is possible to explore the Base Class
Library to see how the HPA attributes are used for various classes and
methods. For instance, the Monitor class is decorated with the following attribute that controls host access: [ComVisible(true), HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)].
The Quest for Code Safety
You might be
wondering why I'm covering the internals of the SQLCLR permission sets
and how their exceptions differ, when fixing the exceptions is so easy:
simply raise the permission level of the assemblies to EXTERNAL_ACCESS or UNSAFE
and give the code access to do what it needs to do. The fact is,
raising the permission levels will certainly work, but by doing so you
may be circumventing the security policy, instead of working with it to
make your system more secure.
As mentioned in
the previous section, code access permissions are granted at the
assembly level rather than the method or line level. Therefore, raising
the permission of a given assembly in order to make a certain module
work can actually affect many different modules contained within the
assembly, giving them all enhanced access. Granting additional
permissions on several modules within an assembly can in turn create a
maintenance burden: if you want to be certain that there are no security
problems, you must review each and every line of code in every module
to make sure it's not doing anything it's not supposed to do—you can no
longer trust the engine to check for you.
You might now be thinking
that the solution is simple: split up your methods so that each resides
in a separate assembly, and then grant permissions that way. Then, each
method really will have its own permission set. But even in that case,
permissions may not be granular enough to avoid code review nightmares.
Consider a complex 5,000-line module that requires a single file I/O
operation to read some lines from a text file. By giving the entire
module EXTERNAL_ACCESS permissions, it
can now read the lines from that file. But of course, you still have to
check all of the remaining code to make sure it's not doing anything
unauthorized.
Then there is the
question of the effectiveness of manual code review. Is doing a
stringent review every time any change is made enough to ensure that the
code won't cause problems that would be detected by the engine if the
code was marked SAFE? And do you really want
to have to do a stringent review before deployment every time any
change is made? In the following section, I will show you how to
eliminate many of these problems by taking advantage of assembly
dependencies in your SQLCLR environment.