There are many ways to secure different parts of your application. The security of running code in .NET revolves around the concept of Code Access Security (CAS). CAS determines the trustworthiness of an assembly based upon its origin. For example, code installed locally on the machine is more trusted than code downloaded from the Internet. The runtime will also validate an assembly’s metadata and type safety before that code is allowed to run.
There are many ways to write secure code and protect data using the .NET Framework. In this chapter, we explore such things as controlling access to types, encryption and decryption, random numbers, securely storing data, and using programmatic and declarative security.
You have an existing class that contains sensitive data and you do not want clients to have direct access to any objects of this class directly. Instead, you would rather have an intermediary object talk to the clients and allow access to sensitive data based on the client’s credentials. What’s more, you would also like to have specific queries and modifications to the sensitive data tracked, so that if an attacker manages to access the object, you will have a log of what the attacker was attempting to do.
Use the proxy design pattern to allow clients to talk directly to a proxy
object. This proxy object will act as gatekeeper to the class that
contains the sensitive data. To keep malicious users from accessing
the class itself, make it private, which will at least keep code
without the
ReflectionPermissionFlag.TypeInformation
access
(which is currently given only in fully trusted code scenarios like
executing code interactively on a local machine ) from getting at it.
The namespaces we will be using are:
using System; using System.IO; using System.Security; using System.Security.Permissions; using System.Security.Principal;
We start this design by creating an interface that will be common to both the proxy objects and the object that contains sensitive data:
internal interface ICompanyData { string AdminUserName { get; set; } string AdminPwd { get; set; } string CEOPhoneNumExt { get; set; } void RefreshData( ); void SaveNewData( ); }
The CompanyData
class is the underlying object
that is “expensive” to create:
internal class CompanyData : ICompanyData { public CompanyData( ) { Console.WriteLine("[CONCRETE] CompanyData Created"); // Perform expensive initialization here } private string adminUserName = "admin"; private string adminPwd = "password"; private string ceoPhoneNumExt = "0000"; public string AdminUserName { get {return (adminUserName);} set {adminUserName = value;} } public string AdminPwd { get {return (adminPwd);} set {adminPwd = value;} } public string CEOPhoneNumExt { get {return (ceoPhoneNumExt);} set {ceoPhoneNumExt = value;} } public void RefreshData( ) { Console.WriteLine("[CONCRETE] Data Refreshed"); } public void SaveNewData( ) { Console.WriteLine("[CONCRETE] Data Saved"); } }
The following is the code for the security proxy class, which checks
the caller’s permissions to determine whether the
CompanyData
object should be created and its
methods or properties called:
public class CompanyDataSecProxy : ICompanyData { public CompanyDataSecProxy( ) { Console.WriteLine("[SECPROXY] Created"); // Must set principal policy first AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy. WindowsPrincipal); } private ICompanyData coData = null; private PrincipalPermission admPerm = new PrincipalPermission(null, @"BUILTINAdministrators", true); private PrincipalPermission guestPerm = new PrincipalPermission(null, @"BUILTINGuest", true); private PrincipalPermission powerPerm = new PrincipalPermission(null, @"BUILTINPowerUser", true); private PrincipalPermission userPerm = new PrincipalPermission(null, @"BUILTINUser", true); public string AdminUserName { get { string userName = ""; try { admPerm.Demand( ); Startup( ); userName = coData.AdminUserName; } catch(SecurityException e) { Console.WriteLine("AdminUserName_get failed! {0}",e.ToString( )); } return (userName); } set { try { admPerm.Demand( ); Startup( ); coData.AdminUserName = value; } catch(SecurityException e) { Console.WriteLine("AdminUserName_set failed! {0}",e.ToString( )); } } } public string AdminPwd { get { string pwd = ""; try { admPerm.Demand( ); Startup( ); pwd = coData.AdminPwd; } catch(SecurityException e) { Console.WriteLine("AdminPwd_get Failed! {0}",e.ToString( )); } return (pwd); } set { try { admPerm.Demand( ); Startup( ); coData.AdminPwd = value; } catch(SecurityException e) { Console.WriteLine("AdminPwd_set Failed! {0}",e.ToString( )); } } } public string CEOPhoneNumExt { get { string ceoPhoneNum = ""; try { admPerm.Union(powerPerm).Demand( ); Startup( ); ceoPhoneNum = coData.CEOPhoneNumExt; } catch(SecurityException e) { Console.WriteLine("CEOPhoneNum_set Failed! {0}",e.ToString( )); } return (ceoPhoneNum); } set { try { admPerm.Demand( ); Startup( ); coData.CEOPhoneNumExt = value; } catch(SecurityException e) { Console.WriteLine("CEOPhoneNum_set Failed! {0}",e.ToString( )); } } } public void RefreshData( ) { try { admPerm.Union(powerPerm.Union(userPerm)).Demand( ); Startup( ); Console.WriteLine("[SECPROXY] Data Refreshed"); coData.RefreshData( ); } catch(SecurityException e) { Console.WriteLine("RefreshData Failed! {0}",e.ToString( )); } } public void SaveNewData( ) { try { admPerm.Union(powerPerm).Demand( ); Startup( ); Console.WriteLine("[SECPROXY] Data Saved"); coData.SaveNewData( ); } catch(SecurityException e) { Console.WriteLine("SaveNewData Failed! {0}",e.ToString( )); } } // DO NOT forget to use [#define DOTRACE] to control the tracing proxy private void Startup( ) { if (coData == null) { #if (DOTRACE) coData = new CompanyDataTraceProxy( ); #else coData = new CompanyData( ); #endif Console.WriteLine("[SECPROXY] Refresh Data"); coData.RefreshData( ); } } }
When creating the PrincipalPermissions
as part of
the object construction, we are using string representations of the
built in objects (”BUILTINAdministrators
“) to set
up the principal role. However, the names of these objects may be
different depending on the locale the code runs under. It would be
appropriate to use the
WindowsAccountType.Administrator
enumeration value
to ease localization since this value is defined to represent the
administrator role as well. We used text here to clarify what was
being done and also to access the PowerUsers
role,
which is not available through the
WindowsAccountType
enumeration.
If the call to the CompanyData
object passes
through the CompanyDataSecProxy
, then the user has
permissions to access the underlying data. Any access to this data
may be logged to allow the administrator to check for any attempted
hacking of the CompanyData
object. The following
code is the tracing proxy used to log access to the various method
and property access points in the CompanyData
object (note that the CompanyDataSecProxy
contains
the code to turn on or off this proxy
object):
public class CompanyDataTraceProxy : ICompanyData { public CompanyDataTraceProxy( ) { Console.WriteLine("[TRACEPROXY] Created"); string path = Path.GetTempPath( ) + @"CompanyAccessTraceFile.txt"; fileStream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.None); traceWriter = new StreamWriter(fileStream); coData = new CompanyData( ); } private ICompanyData coData = null; private FileStream fileStream = null; private StreamWriter traceWriter = null; public string AdminPwd { get { traceWriter.WriteLine("AdminPwd read by user."); traceWriter.Flush( ); return (coData.AdminPwd); } set { traceWriter.WriteLine("AdminPwd written by user."); traceWriter.Flush( ); coData.AdminPwd = value; } } public string AdminUserName { get { traceWriter.WriteLine("AdminUserName read by user."); traceWriter.Flush( ); return (coData.AdminUserName); } set { traceWriter.WriteLine("AdminUserName written by user."); traceWriter.Flush( ); coData.AdminUserName = value; } } public string CEOPhoneNumExt { get { traceWriter.WriteLine("CEOPhoneNumExt read by user."); traceWriter.Flush( ); return (coData.CEOPhoneNumExt); } set { traceWriter.WriteLine("CEOPhoneNumExt written by user."); traceWriter.Flush( ); coData.CEOPhoneNumExt = value; } } public void RefreshData( ) { Console.WriteLine("[TRACEPROXY] Refresh Data"); coData.RefreshData( ); } public void SaveNewData( ) { Console.WriteLine("[TRACEPROXY] Save Data"); coData.SaveNewData( ); } }
The proxy is used in the following manner:
// Create the security proxy here CompanyDataSecProxy companyDataSecProxy = new CompanyDataSecProxy( ); // Read some data Console.WriteLine("CEOPhoneNumExt: " + companyDataSecProxy.CEOPhoneNumExt); // Write some data companyDataSecProxy.AdminPwd = "asdf"; companyDataSecProxy.AdminUserName = "asdf"; // Save and refresh this data companyDataSecProxy.SaveNewData( ); companyDataSecProxy.RefreshData( );
Note that as long as the CompanyData
object were
accessible, we could have also written this to access the object
directly:
// Instantiate the CompanyData object directly without a proxy CompanyData companyData = new CompanyData( ); // Read some data Console.WriteLine("CEOPhoneNumExt: " + companyData.CEOPhoneNumExt); // Write some data companyData.AdminPwd = "asdf"; companyData.AdminUserName = "asdf"; // Save and refresh this data companyData.SaveNewData( ); companyData.RefreshData( );
If these two blocks of code are run, the same fundamental actions occur: data is read, data is written, and data is updated/refreshed. This shows us that our proxy objects are set up correctly and function as they should.
The proxy design pattern is useful for several tasks. The most notable, in COM and .NET Remoting, is for marshaling data across boundaries such as AppDomains or even across a network. To the client, a proxy looks and acts exactly the same as its underlying object; fundamentally, the proxy object is a wrapper around the underlying object.
A proxy can test the security and/or identity permissions of the caller before the underlying object is created or accessed. Proxy objects can also be chained together to form several layers around an underlying object. Each proxy could be added or removed depending on the circumstances.
For the proxy object to look and act the same as its underlying
object, both should implement the same interface. The implementation
in this recipe uses an ICompanyData
interface on
both the proxies (CompanyDataSecProxy
and
CompanyDataTraceProxy
) and the underlying object
(CompanyData
). If more proxies are created, they
too need to implement this interface.
The
CompanyData
class represents an expensive object
to create. In addition, this class contains a mixture of sensitive
and nonsensitive data that require permission checks to be made
before the data is accessed. For this recipe, the
CompanyData
class simply contains a group of
properties to access company data and two methods for updating and
refreshing this data. You can replace this class with one of your own
and create a corresponding interface that both the class and its
proxies implement.
The CompanyDataSecProxy
object is the object that a client must interact with. This object is
responsible for determining whether the client has the correct
privileges to access the method or property that it is calling. The
get
accessor of the
AdminUserName
property shows the structure of the
code throughout most of this class:
public string AdminUserName { get { string userName = ""; try { admPerm.Demand( ); Startup( ); userName = coData.AdminUserName; } catch(SecurityException e) { Console.WriteLine("AdminUserName_get Failed!: {0}",e.ToString( )); } return (userName); } set { try { admPerm.Demand( ); Startup( ); coData.AdminUserName = value; } catch(SecurityException e) { Console.WriteLine("AdminUserName_set Failed! {0}",e.ToString( )); } } }
Initially, a single permission (AdmPerm
) is
demanded. If this demand fails, a
SecurityException
, which is handed by the
catch
clause, is thrown. (Other exceptions will be
handed back to the caller.) If the Demand
succeeds, the Startup
method is called. It is in
charge of instantiating either the next proxy object in the chain
(CompanyDataTraceProxy
) or the underlying
CompanyData
object. The choice depends on whether
the DOTRACE
preprocessor symbol has been defined.
You may use a different technique, such as a registry key to turn
tracing on or off, if you wish. Notice that if a security demand
fails, the expensive object CompanyData
is not
created, saving our application time and resources.
This proxy class uses the private field CoData
to
hold a reference to an ICompanyData
type, which
could either be a CompanyDataTraceProxy
or the
CompanyData
object. This reference allows us to
chain several proxies together.
The
CompanyDataTraceProxy
simply logs any access to
the CompanyData
object’s
information to a text file. Since this proxy will not attempt to
prevent a client from accessing the CompanyData
object, the CompanyData
object is created and
explicitly called in each property and method of this object.