Safety in Windows

Manage Access to Windows Objects with ACLs and the .NET Framework

Mark Novak

This article is based on a prerelease version of the .NET Framework 2.0, formerly code-named "Whidbey." All information contained herein is subject to change.

This article discusses:
  • .NET Framework classes for securing files, directories, registry keys, and other objects
  • How allow and deny rules work
  • Inheritance and propagation of security settings
  • Auditing rules and object ownership
This article uses the following technologies:
Security, C#, .NET Framework

Contents

The Care and Feeding of Security Settings
To Allow and Deny
Got Inheritance?
Parents and Children
A Brief Treatise on Owners
Audit Rules
Taking Snapshots of Security Settings
What Happened to Security Descriptors?
Conclusion

These days, it's not uncommon for a computer system to be accessed by a variety of users, both locally and over a network. Any data these users generate must be kept separate and private. For quite some time, designers of multiuser operating systems have recognized the need for controlling access to files, directories, synchronization objects, network shares, and the like. For instance, in Windows®, each user who logs onto a particular computer is usually assigned her own separate My Documents folder and is prevented from reading and writing to other users' files, unless explicit provisions are made. Student users may be required to submit assignments into a directory while restricted from altering its contents or reading other students' work. A publishing company may have a policy that allows only employees in the desktop publishing department to access the color printer.

Object security is not limited to individual objects—sometimes it applies to entire hierarchies. For example, per company policy the Windows source code on my development machine is accessible only to developers in the Windows organization. The list of applications for object security is virtually inexhaustible.

Support for securing objects has been provided by the Windows family of operating systems for some time. The functionality first appeared in Windows NT®, and evolved only slightly in subsequent releases (Windows 2000, Windows XP, and Windows Server™ 2003). Today, you can secure NTFS files and directories, printers, network shares, Active Directory® objects, and kernel objects, to name a few. The list of securable object types grows all the time. Most often, new security schemes invented by application developers are based on the same set of techniques as the security of existing objects. For example, access control lists (ACLs) that define the security of files and Microsoft® Exchange Server mailboxes are very similar.

Veteran developers using Windows are very familiar with the Windows API support for securing objects, and many articles have been written about access control entries (ACE), ACLs, Security Descriptors, and the Security Descriptor Definition Language (SDDL). However, despite the prominence of object security, manipulating security settings remains a rather difficult development task, even with the gradual introduction of new helper functions by the Windows SDK.

Fortunately, the .NET Framework 2.0 provides an easy, uniform, and extensible way of manipulating the security settings of files, registry keys, directory service containers, and other objects. This article presents the new functionality planned for the Framework and illustrates it with code samples.

The Care and Feeding of Security Settings

Until now, Microsoft did not provide explicit support in the .NET Framework for manipulating security settings. With the .NET Framework 1.x, access can only be granted to users via a series of cumbersome platform invocation (P/Invoke) calls. To make matters worse, not only does this require familiarity with many intricate details pertaining to Windows ACLs and security descriptors, but the resulting code may be prone to a variety of programming errors, some with serious security implications. This is the area where you can find some of the most serious security holes in any application. In perhaps the most famous example of a pitfall of this sort, many readers will recall that setting a NULL discretionary access control list (DACL) on an object grants everyone full access, while an empty DACL does not grant anyone any access.

The .NET Framework 2.0 allows developers to manipulate security settings of objects in a few easy steps using managed code. It does so by introducing the concepts of security objects and rules. The framework classes I will review in this article populate the new System.Security.AccessControl namespace.

Most of my examples will use the file system for illustration. Other objects are secured in much the same way as files, but the file system is the best subject of illustration because it consists of both leaf objects (files) and container objects (directories). Other types of resource managers often have only one or the other type of objects. For example, the registry is purely hierarchical, while mutexes have no hierarchy.

Let's start by looking at how to grant access to a single file. Under the new scheme, access to files is controlled by adding and removing file access control rules from file security objects (see Figure 1). As you can see, changing a file's permissions is accomplished in several steps. First, a file is opened. Then, the file's security object (of type FileSecurity) is retrieved by a call to the GetAccessControl method. This object contains, among other things, an ordered set of access rules which collectively determine the rights of various users and groups to the file.

Figure 1 Add File Access Control

// Start by opening a file
using(FileStream file = new FileStream(
    @"M:\temp\sample.txt", FileMode.Open, FileAccess.ReadWrite))
{
    // Retrieve the file's security settings
    FileSecurity security = file.GetAccessControl();

    // Create a new rule granting Alice read access
    FileSystemAccessRule rule = new FileSystemAccessRule(
        new NTAccount(@"FABRIKAM\Alice"), FileSystemRights.Read,
        AccessControlType.Allow);

    // Add the rule to the existing rules
    security.AddAccessRule(rule);

    // Persist the changes
    file.SetAccessControl(security);
}

In the example, a new access rule is added to the FileSecurity object, granting a user named Alice from the Fabrikam domain read access to the file. Before the change can take effect, it must be persisted to storage. This final step is accomplished by calling the SetAccessControl method.

The previous example illustrated how you can dole out access to users of existing objects. Another equally important scenario is associating security settings with an object at the time of its creation. There is an important security reason for associating access rules with brand new objects. Securable objects are always created with some default security semantics. By default, objects in a hierarchical resource manager (such as file system or registry) inherit their security settings from their parent. Files inherit their security settings from their parent directory, registry keys inherit their security setting from their parent key, and so on. In contrast, nonhierarchical objects, such as mutexes or semaphores, have their default rights assigned to them using system defaults.

The default rights depend on the type of object being created and may not be what you want. For example, rarely would you deliberately create objects to which everyone has full access, yet that may be exactly what the default security settings specify.

The reason you can't simply create an object with default security settings and modify them later is that securing an object after it has been created leaves open a window of opportunity (between creation and modification) when it can be hijacked. Hijacking may lead to a situation where the creator may lose control over a freshly created object, with disastrous consequences. Figure 2 illustrates how to secure a brand new object.

Figure 2 Securing a New Object

// Create a FileSecurity object
FileSecurity security = new FileSecurity();

// Create a new rule granting Alice read access
FileSystemAccessRule rule = new FileSystemAccessRule(
    new NTAccount(@"FABRIKAM\Alice"), FileSystemRights.Read,
    AccessControlType.Allow );

// Add the rule
security.AddAccessRule(rule);

// Create the file passing in the security settings
FileStream file = new FileStream(
    @"M:\temp\sample.txt", FileMode.CreateNew, FileAccess.ReadWrite,
    FileShare.None, 4096, FileOptions.None, security);

The steps should look familiar by now. The same basic operations are taking place, but the order is different and there is no need to persist the changes (because the object is brand new). Before a file is created, a FileSecurity object is created and populated with the desired access rules. Afterwards, the FileSecurity instance is passed to the file's constructor, and the file is properly secured from the beginning.

To Allow and Deny

There are two types of access rules: "allow" and "deny." You can always determine which type of rule you are looking at by examining the rule's AccessControlType property. By convention, deny rules always take precedence over allow rules. Thus, if you add two rules to an object: "grant everyone read and write access" and "deny Bob write access," Bob will be denied write access despite being a member of a group (everyone) that has that right. He will still be allowed read access, though. Figure 3 shows how you can list an object's access rules.

Figure 3 Listing an Object's Access Rules

// Start by opening a file
using(FileStream file = new FileStream(
    @"M:\temp\sample.txt", FileMode.Open, FileAccess.ReadWrite))
{
    // Retrieve the file's security settings
    FileSecurity security = file.GetAccessControl();

    // List each rule in order
    foreach(FileSystemAccessRule rule in
        security.GetAccessRules(true, true, typeof(NTAccount)))
    {
        Console.WriteLine("Rule {0} {1} access to {2}",
            rule.AccessControlType == AccessControlType.Allow ?
                "grants" : "denies",
            rule.FileSystemRights,
            rule.IdentityReference.ToString());
    }
}

The code in Figure 3 will generate output similar to this:

Rule denies Write, Synchronize access to FABRIKAM\Bob
Rule grants FullControl access to BUILTIN\Administrators
Rule grants FullControl access to NT AUTHORITY\SYSTEM
Rule grants FullControl access to FABRIKAM\Alice
Rule grants ReadAndExecute, Synchronize access to BUILTIN\Everyone

Conveniently, access rules are exposed as a collection. This collection is read-only so all modifications to its rules must be performed through dedicated methods of the FileSecurity object, such as AddAccessRule, SetAccessRule, and RemoveAccessRule. Rule objects inside a collection are also immutable.

To understand why deny rules take precedence over allow rules, you have to know how the access check algorithm works. When an access check is performed, rules are evaluated in the order in which they appear inside the access control list. In the example shown in Figure 3, when Bob's access is checked, the rule denying Bob read access is evaluated before the rule granting BUILTIN\Everyone read and execute access. Once a decision to either allow or deny is made, evaluation stops. This is why deny rules "work." If placed after an allow rule, they would not always perform their desired function.

Just as new access rules can be added, existing ones may be removed. Be aware, though, that there is a difference between removing a right from a user and outright denying that right. For example, suppose that Alice is a member of the "full-time employees" group, and that the security settings are: "Full-time employees can read the file" and "Alice has read and write access." Under this arrangement, removing Alice's read right is going to produce the following rules: "Full-time employees can read the file" and "Alice has write access." This is hardly what you would expect since removal of Alice's read access produced no net effect: Alice can still get read access as a full-time employee. If your goal is to be assured that access is not granted to Alice, the only way to accomplish it is by adding a deny rule.

As an aside, if the object contains no access rules at all, everyone will be denied all access to the object.

Got Inheritance?

The examples we've seen so far were pretty straightforward, in part because they have only dealt with simple leaf objects that have no children. Things get a little more complicated once you move from leaf objects such as files, semaphores, and mutexes to container objects such as directories, registry keys, and active directory containers. The additional complexity stems from the fact that the access rules of a container may be configured to apply not just to the object itself, but to its child objects, child containers, or both. Enter the world of inheritance and propagation settings.

Before we go any further in this discussion, a bit of background is in order. Every access rule is either explicit or inherited (the IsInherited property is used to determine which is which). Explicit rules are those that have been added to an object via an explicit action performed on that object. In contrast, inherited rules come from a parent container. When working with an object, you can only manipulate its explicit rules. When adding a new explicit rule to a container, you can specify two sets of flags: one flag for inheritance and one flag for propagation.

There are two inheritance flags: container inherit (CI) and object inherit (OI). Rules that specify container inherit will apply to children of the present object that are containers. Object inherit rules will apply to child leaf objects. With propagation flags set to None, these relationships are transitive: they will span the entire subtree of the hierarchy below the current container and apply to its children, grandchildren, and so on.

The code sample shown in Figure 4 will grant Alice read rights to all subdirectories of the parent directory. Bob will get write access to all files inside the parent directory. Finally, Carol will get both read and write access to both children directories and files. All access rights granted in this example will apply to the directory object itself as well as to the specified subobjects.

Figure 4 Granting Access Rights

// Retrieve current security settings for the target directory
DirectorySecurity security = Directory.GetAccessControl( @"M:\temp" );

// Create new rules
FileSystemAccessRule ruleAlice = new FileSystemAccessRule(
    new NTAccount(@"FABRIKAM\Alice"), FileSystemRights.Read,
    InheritanceFlags.ContainerInherit, PropagationFlags.None,
    AccessControlType.Allow );

FileSystemAccessRule ruleBob = new FileSystemAccessRule(
    new NTAccount(@"FABRIKAM\Bob"), FileSystemRights.Write,
    InheritanceFlags.ObjectInherit, PropagationFlags.None,
    AccessControlType.Allow );

FileSystemAccessRule ruleCarol = new FileSystemAccessRule(
    new NTAccount(@"FABRIKAM\Carol"),
    FileSystemRights.Read | FileSystemRights.Write,
    InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
    PropagationFlags.None, AccessControlType.Allow );

// Add the rules to the existing security settings
security.AddAccessRule( ruleAlice );
security.AddAccessRule( ruleBob );
security.AddAccessRule( ruleCarol );

// Persist the changes
Directory.SetAccessControl( @"M:\temp", security );

Now that we understand inheritance flags, we can turn our attention to propagation flags. Again, there are two kinds: inherit only (IO) and no-propagate inherit (NP). The inherit only flag simply means that the rule applies to child objects only and not to the object itself. Thus, you can grant rights just to files and directories inside a parent directory without affecting the security semantics of the parent itself. The no-propagate inherit flag is a little trickier and fortunately not very common. Its presence indicates that the rule is not transitive; it will affect children, but not grandchildren. It is worth noting that propagation flags are meaningless without at least one inheritance flag. It is indeed an error to try to specify them in that way.

The diagram in Figure 5 illustrates how various combinations of inheritance and propagation flags affect the container (C), child containers (CC), child objects (CO), grandchild containers (GC), and grandchild objects (GO). For each set of inheritance and propagation flags, Figure 5 shows the affected objects in yellow and unaffected the objects in blue. As you can see, the possible range of options available to you is quite rich.

Figure 5 How Inheritance and Propagation Flags Affect Containers

Figure 5 How Inheritance and Propagation Flags Affect Containers

Figure 6 shows how various combinations of inheritance and propagation flags help address common scenarios. It is interesting to note how the explicit and inherited rules are laid out. When you list access rules of a container, they will form several sections (see Figure 7).

Figure 6 Inheritance and Propagation Flags in Common Scenarios

CI OI IO NP Rights (FileSystemRights enum values) Explanation
+ + +   DeleteSubdirectoriesAndFiles The caller will be able to delete any file or directory within a parent directory, but will not be able to delete the directory itself
+       CreateFiles The caller will be able to create files under this directory or any of its subdirectories
+   +   CreateFiles The caller will be able to create files under any subdirectory of this directory, but not inside the directory itself
  + +   ReadData The caller will be able to read all files under this directory and under any of its subdirectories
  + + + ReadData The caller will be able to read all files under this directory, but not under any of its subdirectories

Figure 7 Container Access Rules

Figure 7** Container Access Rules **

As mentioned already, the order of the rules is important since it determines the precedence and ultimately affects how the object can be accessed. While you cannot change the default order, understanding it is important in decoding what type of access is going to be granted by a set of rules. Most importantly, all inherited rules always follow the explicit rules. This way, explicit rules always take precedence over the inherited ones. In turn, parent rules take precedence over grandparent rules, and so on.

Parents and Children

What happens if you want to shield your object from the security semantics bestowed upon it by its parent? A mechanism does, in fact, exist that allows you to say "the security settings of my parent will apply to me no more." At that point, you can even specify whether you want to keep the inherited rules as they exist at that instance, but refuse to "listen" when the parent's settings change, or you can wipe out all inherited rules outright. This is accomplished by access control protection, as Figure 8 illustrates.

Figure 8 Access Control Protection

// Start by opening a file
using(FileStream file = new FileStream(
    @"M:\temp\sample.txt", FileMode.Open, FileAccess.ReadWrite))
{
    // Retrieve the file's security settings
    FileSecurity security = file.GetAccessControl();

    // Protect the rules from inheriting the parent's security settings
    security.SetAccessRuleProtection(
        true,    // changes from parent won't propagate
        false ); // do not keep current inheritance settings

    // Persist the changes
    file.SetAccessControl(security);
}

Note that while you can use this technique to shield yourself from receiving inheritance from your parent, there is no way to receive inheritance that your parent did not intend for you. Propagation only takes place to objects whose ACLs are not protected. The only thing you can do is take a snapshot of the inheritance settings before the parent changes its mind because once your access rules are protected, they remain that way and the parent cannot override that.

A Brief Treatise on Owners

The concept of "owner" is special to object security. An owner is endowed with special powers; even if the rules associated with the object prevent a user from accessing it, if that user is the owner, he or she can override the existing rules and get back the control of the object. The process for doing so is no different from regular manipulation of access rules.

The security object also allows you to change the owner, but the operating system will prevent others from doing this. Generally, in order to change the owner you must either have the TakeOwnership permission to the object or have the special "Take Ownership" privilege. The following code illustrates how you change the owner (provided you have the right to do so):

// Retrieve the file's security settings
FileSecurity security = file.GetAccessControl();

security.SetOwner(new NTAccount(@"FABRIKAM\Dave"));

// Persist the changes
file.SetAccessControl(security);

You can also look to see who the current owner of an object is or ask for the owner to be returned as a security identifier or a Windows NT account object:

// Request the object's owner as a SID
SecurityIdentifier sid = 
    (SecurityIdentifier)security.GetOwner(typeof(SecurityIdentifier));
// Will display something like "S-1-5-21-8-9-10-10000"
Console.WriteLine(sid.ToString());

// Request the object's owner as a Windows NT Account:
NTAccount nta = (NTAccount)security.GetOwner(typeof(NTAccount));
// Will display something like "FABRIKAM\Alice"
Console.WriteLine(nta.ToString());

Audit Rules

So far, I've talked only about access control rules. In Windows lingo, these form the DACL of the object. The DACL can be changed at the discretion of the owner of the object, hence the term "discretionary." It can also be changed by anyone upon whom the owner has bestowed the right to change the DACL.

The object's security descriptor contains another list of rules called the system access control list (SACL). This list controls what type of auditing will be performed on the object by the system. Auditing is a security-sensitive operation. In Windows, audits can only be generated by the Local Security Authority (LSA), because the LSA is the only component allowed to write entries into the security event log, where audits are stored. Security audits are a very serious business. Audits are intended to be used in computer forensics when trying to analyze, after the fact, who did what and who attempted to do what on the system. Many organizations keep their audit logs for years.

Needless to say, the settings dictating what gets audited are usually kept under strict administrative control. If you try the code examples in this section and run into an UnauthorizedAccessException message, it is likely because the account you're running under does not contain the "Security Privilege." This powerful privilege must be assigned to your account by the local machine policy in order for you to be able to modify or even examine SACLs. Good luck trying to ask your friendly local system administrator to grant you this privilege!

Despite these dire warnings, reading and manipulating audit settings of an object, once you have the necessary privilege, is similar in all respects to modifying access control settings. If you paid close attention to code examples in the beginning of this article, you should experience an odd sensation of déjà vu when you look at Figure 9.

Figure 9 Manipulating Audit Rules

// Start by opening a file
using(FileStream file = new FileStream(
    @"M:\temp\sample.txt",FileMode.Open, FileAccess.ReadWrite))
{
    // Retrieve the file's security settings
    FileSecurity security = file.GetAccessControl(
        AccessControlSections.Audit);

    // Create a new rule to generate audits any time that a full-time
    // employee is denied write access to the file
    FileSystemAuditRule rule = new FileSystemAuditRule(
        new NTAccount( @"FABRIKAM\Full_Time_Employees"),
        FileSystemRights.Write, AuditFlags.Failure);

    // Add the rule to the existing rules
    security.AddAuditRule(rule);

    // Persist the changes
    file.SetAccessControl(security);
}

Auditing settings are expressed as audit rules. You can specify the name of a security principal (user or group) that you want to audit, what types of access you are interested in (such as read, write, and so on), and whether you want audits to be generated on access being granted, denied, or both. For instance, in the example shown in Figure 9, an audit will be generated by the system any time that a full-time employee is denied write access to a file or a directory under a given parent directory. Inheritance flags, propagation flags, and protection settings all work exactly the same with audit rules as they do with access control rules.

Taking Snapshots of Security Settings

The Windows security system keeps security settings of every object alongside that object in its security descriptor. You can access the security settings when you want to either display it or make changes, but generally they stay with the object (such as the file or mutex) being protected.

Sometimes the need arises to take a snapshot of the security settings. This can be done for a variety of reasons. You might want to take an object's security settings and apply them to another object. Or, you might want to persist them inside of an XML file. For these sorts of applications, a special textual representation was created called the Security Descriptor Definition Language (SDDL). This is not so much a language as a gobbledygook string representation that very few people know how to read and interpret. Even at Microsoft, few engineers will be able to make sense of something like the following:

D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GR;;;WD)(A;;GR;;;RC)

Cryptic though it is, SDDL does work and is an excellent way of representing a security descriptor in a compact and quasi-human-readable form. To read and interpret SDDL, you should consult its documentation in MSDN®.

Armed with a FileSecurity object, we can obtain either the binary or SDDL form of its security settings by using the appropriate GetSecurityDescriptorXxxForm method call:

// Retrieve owner and DACL portions as SDDL
string sddl = security.GetSecurityDescriptorSddlForm(
    AccessControlSections.Owner | AccessControlSections.Access);

// Get SACL and DACL portions in binary form
byte[] binary = security.GetSecurityDescriptorBinaryForm(
    AccessControlSections.Audit | AccessControlSections.Access);

What Happened to Security Descriptors?

After reading the preceding pages, any seasoned developer using Windows is probably left wondering where the .NET Framework support is for security descriptors, considering that they have been a fundamental building block of Windows security. Indeed, the Microsoft engineering team tasked with implementing object security for the .NET Framework 2.0 felt that security descriptors were too difficult for an average or even above-average developer to understand. As a result, the team formulated the concepts of security objects and rules.

The security descriptors did not go away entirely, however. The engineering team recognized that advanced developers may require access to security descriptors as well as the ACLs, and ACEs of which they're composed. Indeed, objects such as FileSecurity and FileSystemAccessRule are internally backed by a whole family of public classes implementing security descriptors, ACLs, and ACEs. That entire class hierarchy is too complex to be documented here, but a few things are worth highlighting.

ACEs are represented by a family of classes deriving from a base "GenericAce" class. ACEs are immutable objects providing properties such as access mask, security identifier, and various flags (whether the ACE is inherited, propagation flags, and so on).

ACLs also form a tree of classes, rooted at GenericAcl. An ACL is essentially an ICollection of ACEs, but it provides additional functionality such as converting to and from binary form, which is useful for interoperability with the Windows API. Notable subclasses of GenericAcl include an advanced and powerful RawAcl, allowing arbitrary ordering of all ACE types, and CommonAcl, reflecting properly ordered ACLs containing a well-defined set of ACEs. CommonAcl is what you would see on a garden-variety file in your file system.

Finally, security descriptors, rooted at GenericSecurityDescriptor, aggregate owner and group SIDs, a DACL, SACL, and a set of security descriptor control flags. Security descriptor classes provide methods for converting them to and from SDDL and binary form, among other features. A RawSecurityDescriptor subclass of GenericSecurityDescriptor is a general and powerful tool that allows developers to create arbitrary security descriptors, while CommonSecurityDescriptor mirrors commonly encountered security descriptors, with ACLs in their "canonical order."

All in all, this class library should be suitable for both novice and advanced developers, as it places no practical limitations on the type of manipulations you might want to perform on security descriptors, while retaining a simple and attractive rules-based interface for the most commonly performed operations.

Conclusion

This article illustrated the manner in which objects can be secured in the upcoming release of the .NET Framework 2.0. Developers with experience in implementing access control using the Win32® API will find welcome relief from dangerous and difficult manipulations of low-level data structures that plagued "pure" Windows-based programming of these features. The ease with which the new library of classes allows you to secure objects should lead to improved application security. Low-level functionality was retained behind a simplified veneer so that advanced developers will be able to continue to use features familiar to them and to create security descriptors of arbitrary complexity.

Mark Novak is a Software Design Engineer at Microsoft. He worked on several versions of Microsoft Exchange Server before joining the Windows Core Security Team. Mark can be contacted at markpu@windows.microsoft.com.