Chapter 6. Determining Appropriate Access Control

Microsoft Windows offers many means to limit who has access to what. The most common, and to some extent one of the least understood, means is the access control list (ACL). The ACL is a fundamental part of Microsoft Windows NT, Windows 2000, Windows XP, and Windows .NET Server 2003. Part of my job involves reviewing how products and solutions use access control mechanisms, such as ACLs, to protect resources, such as files and registry entries. In some cases, the access control designs are poor and leave the resources open to attack.

In this chapter, I’ll discuss some of the best practices when determining appropriate access control mechanisms for protecting resources. The topics covered include why ACLs are important, what makes up an ACL, how to choose good ACLs, the creation of ACLs, NULL DACLs (discretionary access control lists) and other dangerous access control entry (ACE) types, and other access control mechanisms.

Why ACLs Are Important

ACLs are quite literally your application’s last backstop against an attack, with the possible exception of good encryption and key management. If an attacker can access a resource, his job is done.

Important

Good ACLs are an incredibly important defensive mechanism. Use them.

Imagine you have some data held in the registry and the ACL on the registry key is Everyone (Full Control), which means anyone can do anything to the data, including read, write, or change the data or deny others access to the data. Look at the following code example, which reads the data from the registry key with the dangerous ACL:

#define MAX_BUFF (64)
#define MY_VALUE "SomeData"

BYTE bBuff[MAX_BUFF];
ZeroMemory(bBuff, MAX_BUFF);

//Open the registry.
HKEY hKey = NULL;
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
                 "Software\Northwindtraders",
                 0,
                 KEY_READ,
                 &hKey) == ERROR_SUCCESS) {

      //Determine how much data to read.
      DWORD cbBuff = 0;
      if (RegQueryValueEx(hKey,
                          MY_VALUE,
                          NULL,
                          NULL,
                          NULL,
                          &cbBuff) == ERROR_SUCCESS) {
          //Now read all the data.
          if (RegQueryValueEx(hKey,
                              MY_VALUE,
                              NULL,
                              NULL,
                              bBuff,
                              &cbBuff) == ERROR_SUCCESS) {
              //Cool!
              //We have read the data from the registry.
          }
      }
}

if (hKey) 
    RegCloseKey(hKey);

This code might look reasonable, but it’s horribly flawed. The code incorrectly assumes that the data held in the registry is no bigger than 64 bytes in size. The first call to RegQueryValueEx reads the data size from the registry, and the second call to RegQueryValueEx reads into the local buffer as many bytes of data as were determined by the first call to RegQueryValueEx. A potential buffer overrun exists if this value is greater than 64 bytes.

How dangerous is this? First the code is bad and should be fixed. (I’ll show you a fix in a moment.) The ACL on the registry key determines the threat potential. If the ACL is Everyone (Full Control), the threat is great because any user can set a buffer greater than 64 bytes on the registry key. Also, the attacker can set the ACL to Everyone (Deny Full Control), which will deny your application access to the data.

If the ACL is Administrators (Full Control) and Everyone (Read), the threat is less severe because only an administrator can set data on the key and change the ACL. Administrators have Full Control, which includes the ability to write an ACL, also called WRITE_DAC. All other users can only read the data. In other words, to force the sample application to fail, you need to be an administrator on the computer. If an attacker is already an administrator on the computer, this is only the start of your problems!

Does this mean that if you have good ACLs you can be a sloppy programmer? Not at all! If you need a reminder of why you must fix the code in this example, refer to the "Use Defense in Depth" section of Chapter 3. Let’s look now at fixing the code.

A Diversion: Fixing the Registry Code

This section has nothing to do with ACLs, but because this is a book about code security, I thought I’d round out the solution. The beginning of the solution is to write some code like this:

//Determine how much data to read.
DWORD cbBuff = 0;
if (RegQueryValueEx(hKey,
                    MY_VALUE,
                    NULL,
                    NULL,
                    NULL,
                    &cbBuff) == ERROR_SUCCESS) {

      BYTE *pbBuff = new BYTE[cbBuff];
      //Now read cbBuff bytes of data. 
      if (pbBuff && RegQueryValueEx(hKey,
                                    MY_VALUE,
                                    NULL,
                                    NULL,
                                    pbBuff,
                                    &cbBuff) == ERROR_SUCCESS) {
          //Cool!
          //We have read the data from the registry.

          //Use data

      }

      delete [] pbBuff;

This code still has a problem, but it’s a different issue. In this case, the code allocates memory dynamically, based on the size of the data, and then reads the data from the registry. If an attacker can write 10 MB of data in the registry, because of a weak ACL she has now forced your application to allocate 10 MB of memory. Imagine the consequences if you do this tens or hundreds of times in your code or if the code is in some kind of loop. Your application could allocate hundreds of megabytes of data because the attacker is forcing the application to read 10 MB per read. Before long the application has run out of memory and the computer has ground to a halt as it pages memory in and out of the swap file.

Personally, the fix I’d make is to use the following code:

BYTE bBuff[MAX_BUFF];
ZeroMemory(bBuff, MAX_BUFF);
HKEY hKey = NULL;
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
                 "Software\Northwindtraders",
                 0,
                 KEY_READ,
                 &hKey) == ERROR_SUCCESS) {

      DWORD cbBuff = sizeof (bBuff);
      //Now read no more than MAX_BUFF bytes of data.
      if (RegQueryValueEx(hKey,
                          MY_VALUE,
                          NULL,
                          NULL,
                          bBuff,
                          &cbBuff) == ERROR_SUCCESS) {
            //Cool!
            //We have read the data from the registry.
      }
}

if (hKey) 
    RegCloseKey(hKey);

In this case, even if an attacker sets a large data value in the registry, the code will read up to MAX_BUFF bytes and no more. If there is more data, RegQueryValueEx will return an error, ERROR_MORE_DATA, indicating the buffer is not large enough to hold the data.

Once again, you can mitigate this threat by using good ACLs on the registry key in question, but you should still fix the code, just in case there’s a poor ACL or the administrator accidentally sets a poor ACL. That’s enough of a detour—let’s get back to ACLs.

What Makes Up an ACL?

The following is a brief overview for those of you who might have forgotten what an ACL is or maybe never knew it in the first place! You can skip this section if you’re familiar with ACLs. An ACL is an access control method employed by many operating systems, including Windows NT, Windows 2000, and Windows XP, to determine to what degree an account is allowed to access a resource. Windows 95, Windows 98, Windows Me, and Windows CE do not support ACLs.

Windows NT and later contain two types of ACLs: discretionary access control lists (DACLs) and system access control list (SACLs). A DACL determines access rights to secured resources. A SACL determines audit policy for secured resources.

Examples of resources that can be secured using DACLs and audited using SACLs include the following:

  • Files and directories

  • File shares (for example, \BlakesLaptopBabyPictures)

  • Registry keys

  • Shared memory

  • Job objects

  • Mutexes

  • Named pipes

  • Printers

  • Semaphores

  • Active directory objects

Each DACL includes zero or more access control entries (ACEs), which I’ll define in a moment. A NULL DACL—that is, a current DACL that is set to NULL— means no access control mechanism exists on the resource. NULL DACLs are bad and should never be used because an attacker can set any access policy on the object. I’ll cover NULL DACLs later in this chapter.

An ACE includes two major components: an account represented by the account’s Security ID (SID) and a description of what that SID can do to the resource in question. As you might know, a SID represents a user, group, or computer. The most famous—some would say infamous—ACE is Everyone (Full Control). Everyone is the account; the SID for Everyone, also called World, is S-1-1-0. Full Control is the degree to which the account can access the resource in question—in this case, the account can do anything to the resource. Believe me, Full Control really does mean anything! Note that an ACE can also be a deny ACE, an ACE that disallows certain access. For example, Everyone (Deny Full Control) means that every account—including you!—will be denied access to the resource. If an attacker can set this ACE on a resource, serious denial of service (DoS) threats exist because no one can access the resource.

Note

The object owner can always get access to the resource, even if the ACL denies him access. All securable objects in Windows have an owner. If you create an object, such as a file, you are the owner. The only exception is an object created by an administrator, in which case all administrators are owners of that object.

A Method of Choosing Good ACLs

Over the past few months I’ve come to live by the following security maxim when performing security reviews: "You must account for every ACE in an ACL." In fact, if you can’t determine why an ACE exists in an ACL, you should remove the ACE from the ACL. As with all engineering processes, you should design your system using a high-level analysis technique to model the business requirements before creating the solution, and the same philosophy applies to creating ACLs. I’ve seen many applications that have ACLs "designed" in an utterly ad hoc manner, and this has led to security vulnerabilities or poor user experiences.

The process of defining an appropriate ACL for your resources is simple:

  1. Determine the resources you use.

  2. Determine the business-defined access requirements.

  3. Determine the appropriate access control technology.

  4. Convert the access requirements to access control technology.

First and foremost, you need to determine which resources you use—for example, files, registry keys, database data, Web pages, named pipes, and so on—and which resources you want to protect. Once you know this, you’ll have a better understanding of the correct ACLs to apply to protect the resources. If you can’t determine what your resources are, ask yourself where the data comes from—that should lead you to the resource.

Next you should determine the access requirements for the resources. Recently I had a meeting with a group that used Everyone (Full Control) on some critical files they owned. The rationale was that local users on the computer needed to access the files. After I probed the team a little more, a team member said the following:

All users at the computer can read the data files. Administrators need to perform all tasks on the files. However, users in accounting should have no access to the files.

Take note of the emphasized (roman) words. For those of you who have used Unified Modeling Language (UML) use cases, you can see what I’m doing—extracting key parts of speech from the scenario to build business requirements. From these business requirements, you can derive technical solutions—in this case, access requirements used to derive access control lists.

More Information

A useful introduction to UML is UML Distilled: A Brief Guide to the Standard Object Modeling Language, 2nd Edition (Addison-Wesley Publishing Co, 1999), by Martin Fowler and Kendall Scott.

Remember that ACLs are composed of ACEs and that an ACE is a rule in the following form: "A subject can perform an action against an object" or "Someone can perform something on some resource." In our example, we have three ACEs. All users at the computer can read the data files is a rule that translates nicely into the first ACE on the data files: Interactive Users (Read). It’s classic noun-verb-noun. The nouns are your subjects and objects, and the verb determines what the ACE access mask should be. The access mask is a 32-bit value that defines the rights that are allowed or denied in an ACE.

Note

The Interactive Users group SID applies to any user logged on to a system with a call to LogonUser when dwLogonType is LOGON32_LOGON_INTERACTIVE.

Interactive Users is the same as All users at the computer. Also, users who are accessing the computer via FTP or HTTP and are authenticated using Basic authentication are logged on interactively by default when using Internet Information Services (IIS) 5.

You should follow this process for all subjects (users, groups, and computers) until you create a complete ACL. In this example, we end up with the ACL shown in Table 6-1.

Table 6-1. Access Control List Derived from Business Requirements

Subject

Access Rights

Accounting

Deny All Access

Interactive Users

Read

Administrators

Full Control

SYSTEM

Full Control

Important

When building ACLs using code, you should always place deny ACEs at the start of the ACL. ACLs built using the Windows ACL user interface will do this for you. Failure to place deny ACEs before allow ACEs might grant access that should not be allowed.

I once filed a bug against a team that had an Everyone (Full Control) ACL on a named pipe the application created. The developer closed the bug as By Design, citing that everyone had to read, write, and synchronize to the pipe. It was fun reopening the bug and telling the developer that she had just defined what the ACL should be!

Note

Good ACLs are paramount if your application might be running in a Terminal Server environment. Many users might have access to more code-based resources, such as named pipes and shared memory, and poor ACLs can increase the chance that malicious users can affect the system’s operation by denying other access to resources.

More Information

Take a look at the "Weak Permissions on Winsock Mutex Can Allow Service Failure" Microsoft security bulletin (MS01-003), issued in January 2001 and available at http://www.microsoft.com/technet/security, for information about the implications of weak ACLs and Terminal Server.

Effective Deny ACEs

Sometimes, when defining the access policy for resources, you’ll decide that some users should have no access to a resource. In that case, don’t be afraid to use a deny ACE.

Determining access control requirements is as simple as writing out the access control rules—again, based on the business rules—for the application and then looking for verbs and nouns in the requirements. Then you can determine which access control technologies are appropriate and how to configure the mechanisms to comply with the access control policy.

Creating ACLs

I’m covering the creation of ACLs because one of the arguments I hear from developers against adding ACLs to their applications is that they have no idea which APIs to use. In this portion of the chapter, I’ll delve into creating ACLs in Windows NT 4 and Windows 2000, and I’ll explore some new functionality in Visual Studio .NET and the Active Template Library (ATL).

Creating ACLs in Windows NT 4

I remember the first time I used ACLs in some C++ code, and it was daunting. At that point I realized why so many people don’t bother creating good ACLs—it’s a complex task, requiring lots of error-prone code. If it makes you feel any better, the following example code is for Windows NT 4 and later. (The code for versions of Windows NT prior to version 4 would be even more complex, involving calls to malloc and AddAce!) The code shows how to create an ACL and, in turn, a security descriptor, which is then applied to a newly created directory. Note that the directory will already have an ACL inherited from the parent directory. This code overrides that ACL. Frankly, I never rely on default ACLs inherited from a parent container—you never know whether someone has set poor ACLs.

/*
  NT4ACL.cpp 
*/

#include <windows.h>
#include <stdio.h>
#include <aclapi.h>

PSID pEveryoneSID = NULL, pAdminSID = NULL, pNetworkSID  = NULL;
PACL pACL = NULL;
PSECURITY_DESCRIPTOR pSD = NULL;

//ACL will contain three ACEs:
//   Network (Deny Access)
//   Everyone (Read)
//   Admin (Full Control)
try { 
    const int NUM_ACES = 3;
    EXPLICIT_ACCESS ea[NUM_ACES];
    ZeroMemory(&ea, NUM_ACES * sizeof(EXPLICIT_ACCESS)) ;

    //Create a well- known SID for the Network logon group.
    SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY;
    if (!AllocateAndInitializeSid(&SIDAuthNT, 1,
                                  SECURITY_NETWORK_RID,
                                  0, 0, 0, 0, 0, 0, 0,
                                  &pNetworkSID) ) 
        throw GetLastError();

    ea[0].grfAccessPermissions = GENERIC_ALL;
    ea[0].grfAccessMode = DENY_ACCESS;
    ea[0].grfInheritance= NO_INHERITANCE;
    ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
    ea[0].Trustee.ptstrName  = (LPTSTR) pNetworkSID;

    //Create a well-known SID for the Everyone group.
    SID_IDENTIFIER_AUTHORITY SIDAuthWorld =
                SECURITY_WORLD_SID_AUTHORITY;
    if (!AllocateAndInitializeSid(&SIDAuthWorld, 1,
                                  SECURITY_WORLD_RID,
                                  0, 0, 0, 0, 0, 0, 0,
                                  &pEveryoneSID) ) 
        throw GetLastError();

    ea[1].grfAccessPermissions = GENERIC_READ;
    ea[1].grfAccessMode = SET_ACCESS;
    ea[1].grfInheritance= NO_INHERITANCE;
    ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea[1].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
    ea[1].Trustee.ptstrName  = (LPTSTR) pEveryoneSID;

    //Create a SID for the BUILTINAdministrators group.
    if (!AllocateAndInitializeSid(&SIDAuthNT, 2,
                                  SECURITY_BUILTIN_DOMAIN_RID,
                                  DOMAIN_ALIAS_RID_ADMINS,
                                  0, 0, 0, 0, 0, 0,
                                  &pAdminSID) ) 
        throw GetLastError(); 

    ea[2].grfAccessPermissions = GENERIC_ALL;
    ea[2].grfAccessMode = SET_ACCESS;
    ea[2].grfInheritance= NO_INHERITANCE;
    ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea[2].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
    ea[2].Trustee.ptstrName  = (LPTSTR) pAdminSID;

    //Create a new ACL with the three ACEs.
    if (ERROR_SUCCESS != SetEntriesInAcl(NUM_ACES, 
        ea, 
        NULL, 
        &pACL)) 
        throw GetLastError();

    //Initialize a security descriptor.  
    pSD = (PSECURITY_DESCRIPTOR) LocalAlloc(LPTR, 
                               SECURITY_DESCRIPTOR_MIN_LENGTH); 
    if (pSD == NULL) 
        throw GetLastError();

    if (!InitializeSecurityDescriptor(pSD, 
                               SECURITY_DESCRIPTOR_REVISION))
        throw GetLastError(); 

    // Add the ACL to the security descriptor. 
    if (!SetSecurityDescriptorDacl(pSD, 
                                   TRUE,     //  fDaclPresent flag   
                                   pACL, 
                                   FALSE)) {  
        throw GetLastError(); 
    } else {
        SECURITY_ATTRIBUTES sa;
        sa.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa.bInheritHandle = FALSE;
        sa.lpSecurityDescriptor = pSD;

        if (!CreateDirectory("C:\Program Files\MyStuff", &sa)) 
            throw GetLastError();
    } // End try
} catch(...) {
    // Error condition
}

if (pSD) 
    LocalFree(pSD);

if (pACL)
    LocalFree(pACL);

//  Call FreeSID for each SID allocated by AllocateAndInitializeSID.
if (pEveryoneSID) 
    FreeSid(pEveryoneSID);

if (pNetworkSID)
    FreeSid(pNetworkSID);

if (pAdminSID) 
    FreeSid(pAdminSID);

This sample code is also available in the companion content in the folder Secureco2Chapter06. As you can see, the code is not trivial, so let me explain what’s going on. First you need to understand that you do not apply an ACL directly to an object—you apply a security descriptor (SD). The SD is encapsulated in a SECURITY_ATTRIBUTES structure, which contains a field that determines whether the SD is inherited by the process. A security descriptor includes information that specifies the following components of an object’s security:

  • An owner (represented by a SID), set using SetSecurityDescriptorOwner.

  • A primary group (represented by a SID), set using SetSecurityDescriptorGroup.

  • A DACL, set using SetSecurityDescriptorDacl.

  • An SACL, set using SetSecurityDescriptorSacl.

If any of the components of a security descriptor are missing, defaults are used. For example, the default owner is the same as the identity of the process calling the function or the Builtin Administrators group if the caller is a member of that group. In the preceding example, only the DACL is set. As mentioned, the security descriptor contains a DACL, and this is made up of one or more EXPLICIT_ACCESS structures. Each EXPLICIT_ACCESS structure represents one ACE. Finally, each EXPLICIT_ACCESS structure contains a SID and which permissions that SID has when attempting to use the object. The EXPLICIT_ACCESS structure also contains other details, such as whether the ACE is to be inherited. The process of creating an ACL is also illustrated in Figure 6-1.

Two other APIs exist for setting ACLs on files: SetFileSecurity and SetNamedSecurityInfo. SetFileSecurity is available in all versions of Windows NT, and SetNamedSecurityInfo is available in Windows NT 4 and later.

The process of creating an ACL.

Figure 6-1. The process of creating an ACL.

If your application runs on Windows 2000 or later, there is some relief from such code in the form of the Security Descriptor Definition Language, covered next.

Creating ACLs in Windows 2000

Recognizing that many people did not understand the ACL and security descriptor functions in Windows NT 4, the Windows 2000 security engineering team added a textual ACL and security descriptor representation called the Security Descriptor Definition Language (SDDL). Essentially, SIDs and ACEs are represented in SDDL through the use of well-defined letters.

More Information

Full details of the SDDL can be found in Sddl.h, available in the Microsoft Platform SDK.

The following example code creates a directory named c:MyDir and sets the following ACE:

  • Guests (Deny Access)

  • SYSTEM (Full Control)

  • Administrators (Full Control)

  • Interactive Users (Read, Write, Execute)

/*
  SDDLACL.cpp
*/

#define _WIN32_WINNT 0x0500

#include <windows.h>
#include <sddl.h>

void main() {
    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.bInheritHandle = FALSE;
    char *szSD = "D:P"                      //DACL
         "(D;OICI;GA;;;BG)"         //Deny Guests
         "(A;OICI;GA;;;SY)"         //Allow SYSTEM Full Control
         "(A;OICI;GA;;;BA)"         //Allow Admins Full Control
         "(A;OICI;GRGWGX;;;IU)";    //Allow Interactive Users RWX

    if (ConvertStringSecurityDescriptorToSecurityDescriptor(
        szSD,
        SDDL_REVISION_1, 
        &(sa.lpSecurityDescriptor), 
        NULL)) {

        if (!CreateDirectory("C:\MyDir", &sa )) {
            DWORD err = GetLastError();
        }

        LocalFree(sa.lpSecurityDescriptor);
   } 
}

This code is significantly shorter and easier to understand than that in the Windows NT 4 example. Needing some explanation, however, is the SDDL string in the szSD string. The variable szSD contains an SDDL representation of the ACL. Table 6-2 outlines what the string means. You can also find this sample code in the companion content in the folder Secureco2Chapter06.

Table 6-2. Analysis of an SDDL String

SDDL Component

Comments

D:P

This is a DACL. Another option is S: for audit ACE (SACL). The ACE follows this component. Note that the P option sets SE_DACL_PROTECTED, which gives you maximum control of the ACEs on the object by preventing ACEs from being propagated to the object by a parent container. If you don’t care that ACEs are inherited from a parent, you can remove this option.

(D;OICI;GA;;;BG)

An ACE string. Each ACE is wrapped in parentheses.

D = deny ACE.

OICI = perform object and container inheritance. In other words, this ACE is set automatically on objects (such as files) and containers (such as directories) below this object or container.

GA = Generic All Access (Full Control).

BG = Guests group (also referred to as Builtin Guests).

This ACE prevents the guest account from accessing this directory or any file or subdirectory created beneath it.

The two missing values represent ObjectTypeGuid and Inherited­ObjectTypeGuid, respectively. They are not used in this example because they apply only to object-specific ACEs. Object-specific ACEs allow you to have greater granularity control for the types of child objects that can inherit them.

(A;OICI;GA;;;SY)

A = allow ACE.

SY = SYSTEM (also called the local system account).

(A;OICI;GA;;;BA)

BA = Builtin Administrators group.

(A;OICI;GRGWGX;;;IU)

GR = Read, GW = Write, GX = Execute.

IU = Interactive users (users logged on at the computer).

Figure 6-2 shows the general layout of the SDDL string in the previous sample code.

The sample SDDL string explained.

Figure 6-2. The sample SDDL string explained.

No doubt you’ll need to use other User accounts and Builtin accounts, so Table 6-3 presents a partial list of the well-known SIDs in Windows 2000 and later.

Table 6-3. SDDL SID Types 

SDDL String

Account Name

AO

Account Operators

AU

Authenticated Users

BA

Builtin Administrators

BG

Builtin Guests

BO

Backup Operators

BU

Builtin Users

CA

Certificate Server Administrators

CO

Creator Owner

DA

Domain Administrators

DG

Domain Guests

DU

Domain Users

IU

Interactively Logged-On User

LA

Local Administrator

LG

Local Guest

NU

Network Logon User

PO

Printer Operators

PU

Power Users

RC

Restricted Code—a restricted token, created using the CreateRestrictedToken function in Windows 2000 and later

SO

Server Operators

SU

Service Logon User—any account that has logged on to start a service

SY

Local System

WD

World (Everyone)

NS

Network Service (Windows XP and later)

LS

Local Service (Windows XP and later)

AN

Anonymous Logon (Windows XP and later)

RD

Remote Desktop and Terminal Server users (Windows XP and later)

NO

Network Configuration Operators (Windows XP and later)

LU

Logging Users (Windows .NET Server and later)

MU

Monitoring Users (Windows .NET Server and later)

The advantage of SDDL is that it can be persisted into configuration files or XML files. For example, SDDL is used by the Security Configuration Editor .inf files to represent ACLs for the registry and NTFS.

More Information

During the Windows Security Push, access to the performance counters was tightened, which led to the creation of the Logging Users and Monitoring Users groups.

Creating ACLs with Active Template Library

The ATL is a set of template-based C++ classes included with Microsoft Visual Studio 6 and Visual Studio .NET. A new set of security-related ATL classes have been added to Visual Studio .NET to make managing common Windows security tasks, including ACLs and security descriptors, much easier. The following sample code, created using Visual Studio .NET, creates a directory and assigns an ACL to the directory. The ACL is

  • Blake (Read)

  • Administrators (Full Control)

  • Guests (Deny Access)

/*
  ATLACL.cpp 
*/

#include <atlsecurity.h>
#include <iostream>

using namespace std;

void main(){

    try {
        //The user accounts
        CSid sidBlake("Northwindtraders\blake");
        CSid sidAdmin = Sids::Admins();
        CSid sidGuests = Sids::Guests();

        //Create the ACL, and populate with ACEs.
        //Note the deny ACE is placed before the allow ACEs.
        CDacl dacl;
        dacl.AddDeniedAce(sidGuests, GENERIC_ALL);
        dacl.AddAllowedAce(sidBlake, GENERIC_READ);
        dacl.AddAllowedAce(sidAdmin, GENERIC_ALL);

        //Create the security descriptor and attributes.
        CSecurityDesc sd;
        sd.SetDacl(dacl);
        CSecurityAttributes sa(sd);

        //Create the directory with the security attributes.
        if (CreateDirectory("c:\MyTestDir", &sa))
            cout << "Directory created!" << endl;

    } catch(CAtlException e) {
        cerr << "Error, application failed with error " 
             << hex << (HRESULT)e << endl;
    }
}

Note

Note the use of Sids::Admins() and Sids::Guests() in the code. You should use these these values when dealing with well-known SIDs rather than the English names ("Administrators" and "Guests") because the names might not be valid and the code will fail when running on non-English versions of Windows. You can view a list of all the well-known SIDs in the Sids C++ namespace in atlsecurity.h.

In my opinion, this code is much easier to understand than both the Windows NT 4 and Windows 2000 SDDL versions. It’s easier than the Windows NT 4 code because it’s less verbose, and it’s easier than the Windows 2000 SDDL code because it’s less cryptic. This sample code is also available in the companion content in the folder Secureco2Chapter06.

Now that I’ve discussed how to define good ACLs for your application and methods for creating them, let’s look at some common mistakes made when creating ACLs.

Getting the ACE Order Right

I’ve already touched on getting the ACE ordering correct in the ACL. If you use the Windows ACL dialog boxes, the operating system will always order ACEs correctly. However, you do not have this luxury when writing code to build ACLs, and it’s imperative that you get the order right. This is especially important when your code reads an ACL from a resource, such as a registry key, adds an ACE, and then updates the registry. If you’re building ACLs in code, the correct ACE order is

  • Explicit Deny

  • Explicit Allow

  • Inherited Deny from parent

  • Inherited Allow from parent

  • Inherited Deny from grandparent

  • Inherited Allow from grandparent

  • Inherited Deny from great grand-parent

  • Inherited Allow from great grandparent

  • and so on.

Perform the following steps to correctly add a new ACE to an existing ACL:

  1. Use the GetSecurityInfo or GetNamedSecurityInfo function to get the existing ACL from the object’s security descriptor.

  2. For each new ACE, fill an EXPLICIT_ACCESS structure with the information that describes the ACE.

  3. Call SetEntriesInAcl, specifying the existing ACL and an array of EXPLICIT_ACCESS structures for the new ACEs.

  4. Call the SetSecurityInfo or SetNamedSecurityInfo function to attach the new ACL to the object’s security descriptor.

The following C++ code outlines the process. Note that it uses a new function, CreateWellKnownSid (added to Windows 2000 SP3, Windows XP, and Windows .NET Server), which is similar to the ATL CSid class.

/*
SetUpdatedACL.cpp
*/

#define _WIN32_WINNT 0x0501
#include "windows.h"
#include "aclapi.h"
#include <sddl.h>

int main(int argc, char* argv[]) {
        char *szName = "c:\junk\data.txt";
        PACL pDacl = NULL;
        PACL pNewDacl = NULL;
        PSECURITY_DESCRIPTOR sd = NULL;
        PSID sidAuthUsers = NULL;
        DWORD dwErr = 0;
    
        try {
                dwErr = 
                        GetNamedSecurityInfo(szName,
                                        SE_FILE_OBJECT,
                                        DACL_SECURITY_INFORMATION,
                                        NULL,
                                        NULL,
                                        &pDacl,
                                        NULL,
                                        &sd);
                if (dwErr != ERROR_SUCCESS) 
                        throw dwErr;

                EXPLICIT_ACCESS ea;
                ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS));
        
                DWORD cbSid = SECURITY_MAX_SID_SIZE;
                sidAuthUsers = LocalAlloc(LMEM_FIXED,cbSid);
                if (sidAuthUsers == NULL) 
                        throw ERROR_NOT_ENOUGH_MEMORY;

                if (!CreateWellKnownSid(WinAuthenticatedUserSid,
                                            NULL,
                                            sidAuthUsers,
                                            &cbSid))
                        throw GetLastError();

                    BuildTrusteeWithSid(&ea.Trustee, sidAuthUsers);
                    ea.grfAccessPermissions = GENERIC_READ;
                    ea.grfAccessMode        = SET_ACCESS;
                    ea.grfInheritance       = NO_INHERITANCE;
                    ea.Trustee.TrusteeForm  = TRUSTEE_IS_SID;
                    ea.Trustee.TrusteeType  = TRUSTEE_IS_GROUP;

                    dwErr = SetEntriesInAcl(1,&ea,pDacl,&pNewDacl);
                    if (dwErr != ERROR_SUCCESS)
                            throw dwErr;

                    dwErr = 
                        SetNamedSecurityInfo(szName,
                                    SE_FILE_OBJECT,
                                    DACL_SECURITY_INFORMATION,
                                    NULL,
                                    NULL,
                                    pNewDacl,
                                    NULL);
            } catch(DWORD e) {
                    //error
            }

            if (sidAuthUsers) 
                    LocalFree(sidAuthUsers);
    
            if (sd) 
                    LocalFree(sd);
    
            if (pNewDacl) 
                LocalFree(pNewDacl);

        return dwErr;
}

Note that functions such as AddAccessAllowedAceEx and AddAccessAllowedObjectAce add an ACE to the end of an ACL. It is the caller’s responsibility to ensure that the ACEs are added in the proper order.

Finally, be wary of AddAccessAllowedACE because it does not allow you to control ACL inheritance. Instead, you should use AddAccessAllowedACEEx.

Be Wary of the Terminal Server and Remote Desktop SIDs

Windows offers the well-known Terminal Server and Remote Desktop Users SIDs that are present in a user’s token if they log on using Terminal Server (Windows 2000 Server) or the Remote Desktop (Windows XP and later). Because the SID is in the user’s token, you can use it to control access to resources by creating an ACL such as this:

  • Administrators (Full Control)

  • Remote Desktop Users (Read)

  • Interactive Users (Read, Write)

Be aware that the user’s token may not include the Remote Desktop Users SID if the user was previously interactively logged on at the computer. Let me explain by way of a scenario:

  • Madison logs on to her computer at work and performs her normal tasks. Her token includes the Interactive User SID because she is physically logged on at the computer.

  • She locks the workstation and goes home for the evening.

  • From home, she decides to connect to the work computer by using the Remote Desktop feature of Windows XP through a VPN.

  • When she connects to the computer, the work computer logs her on, creating a new token in the process that includes the Remote Desktop Users token. The software then realizes Madison is already logged on and has an active session, so to preserve the state of the desktop as she left it, the Terminal Server code throws the new token away and piggybacks the existing interactive session.

At this point, as far as the operating system is concerned, Madison is an interactive user.

As an interactive user, Madison has read and write access to the object, rather than just read access. This is not as bad as it sounds because she has read and write access anyway when she is logged on physically at the computer. Also, in instances where the computer is accessible only remotely, she will never have an interactive session.

Of course, the cynics among you will say that Madison is probably an administrator on her own computer anyway, so why bother with other SIDs in the token!

The lesson here is be aware of this issue when building ACLs.

NULL DACLs and Other Dangerous ACE Types

A NULL DACL is a way of granting all access to all users of an object, including attackers. I sometimes quip that NULL DACL == No Defense. And it is absolutely true. If you don’t care that anyone can do anything to your object—including read from it, write to it, delete existing data, modify existing data, and deny others access to the object—a NULL DACL is fine. However, I have yet to see a product for which such a requirement is of benefit, which, of course, completely rules out the use of NULL DACLs in your products!

If you see code like the following, file a bug. It should be fixed because the object is not protected.

if (SetSecurityDescriptorDacl(&sd,  
                              TRUE,     //DACL Present              
                              NULL,     //NULL DACL  
                              FALSE)) {                
    //Use security descriptor and NULL DACL.
}

Another variation of this is to populate a SECURITY_DESCRIPTOR structure manually. The following code will also create a NULL DACL:

SECURITY_DESCRIPTOR sd = {
                          SECURITY_DESCRIPTOR_REVISION,
                          0x0,
                          SE_DACL_PRESENT,
                          0x0,
                          0x0,
                          0x0,
                          0x0};      //Dacl is 0, or NULL.

Note

A debug version of your application will assert if you create a NULL DACL by using the ATL library included with Visual Studio .NET.

While working on Windows XP, I and others on the Secure Windows Initiative team and Windows Security Penetration Team spent many hours looking for NULL DACLs, filing bugs against the code owners, and getting them fixed. Then we spent time analyzing why people created objects with NULL DACLs in the first place. We found two reasons:

  • Developers were overwhelmed by the code required to create ACLs. Hopefully, you understand at least one of the three options I have covered earlier in this chapter and can create code to reflect the ACLs you need.

  • The developer thought that a NULL DACL would be "good enough" because his code always worked when the NULL DACL was used. By now, you know this is a bad thing because if it works so well for users, it probably works just as well for attackers!

Frankly, I think both of these reveal a touch of laziness or perhaps lack of knowledge. It’s true that defining a good ACL takes a little work, but it is well worth the effort. If your application is attacked because of a weak ACL, you will have to patch your code anyway. You may as well get it right now.

Note

A NULL DACL is not the same as a NULL security descriptor. If the SD is set to NULL when creating an object, the operating system will create a default SD including a default DACL, usually inherited from the object’s parent.

I once wrote a simple tool in Perl to look for NULL DACLs in C and C++ source code. I used the tool to analyze some source code from a Microsoft partner and found about a dozen NULL DACLs. After filing the bugs and waiting for them to be fixed, I ran the tool again to verify that they had been fixed, and indeed, the tool a second time yielded no more NULL DACLs. Almost three months after filing the bugs, I performed a security source code audit and saw that the code for one of the NULL DACL bugs looked strange. It had changed from

SetSecurityDescriptorDacl(&sd,
                          TRUE, 
                          NULL,     //DACL  
                          FALSE);

to the following, which would not be picked up by the tool:

SetSecurityDescriptorDacl(&sd,  
                          TRUE,
                          ::malloc(0xFFFFFFFF),     //DACL
                          FALSE);

While the code is a silly stunt, it is somewhat clever. If the memory allocation function, malloc, fails to allocate the requested memory block, it returns NULL. The developer is attempting to allocate 0xFFFFFFFF, or 4,294,967,295 bytes of data, which on most machines will fail, and hence the developer set the DACL to NULL! I looked at the bug and saw the developer claimed he had fixed the bug. Of course, I did what comes naturally and reopened the bug and didn’t relax until the code was fixed properly.

NULL DACLs and Auditing

Here’s another insidious aspect of NULL DACLs: if a valid user does indeed change a NULL DACL to Everyone (Deny Access), chances are good that nothing is logged in the Windows event log to indicate this malicious act because the chances are also good that you have no audit ACE (an SACL) on the object either!

Important

NULL DACLs are simply dangerous. If you find a NULL DACL in your application, file a bug and get it fixed.

Dangerous ACE Types

You should be wary of three other dangerous ACE types: Everyone (WRITE_DAC), Everyone (WRITE_OWNER), and directory ACLs, which allow anyone to add new executables.

Everyone (WRITE_DAC)

WRITE_DAC is the right to modify the DACL in the object’s security descriptor. If an untrusted user can change the ACL, the user can give himself whatever access to the object he wants and can deny others access to the object.

Everyone (WRITE_OWNER)

WRITE_OWNER is the right to change the owner in the object’s security descriptor. By definition, the owner of an object can do anything to the object. If an untrusted user can change the object owner, all access is possible for that user as is denying others access to the object.

Everyone (FILE_ADD_FILE)

The Everyone (FILE_ADD_FILE) ACE is particularly dangerous because it allows untrusted users to add new executables to the file system. The danger is that an attacker can write a malicious executable file to a file system and wait for an administrator to run the application. Then the malevolent application, a Trojan, performs nefarious acts. In short, never allow untrusted users to write files to shared application directories.

Everyone (DELETE)

The Everyone (DELETE) ACE allows anyone to delete the object, and you should never allow untrusted users to delete objects created by your application.

Everyone (FILE_DELETE_CHILD)

The Everyone (FILE_DELETE_CHILD) ACE, known as Delete Subfolders And Files in the user interface, is set on container objects, such as directories. It allows a user to delete a child object, such as a file, even if the user does not have access to the child object. If the user has FILE_DELETE_CHILD permission to the parent, she can delete the child object regardless of the permissions on the child.

Everyone (GENERIC_ALL)

GENERIC_ALL, also referred to as Full Control, is as dangerous as a NULL DACL. Don’t do it.

What If I Can’t Change the NULL DACL?

I can think of no reason to create an object with a NULL DACL, other than the case in which it simply doesn’t matter if the object is compromised. I saw an example of this once where a dialog box would pop up to tell the user a joke. It used a mutex, with a NULL DACL to "protect" it, to make sure that multiple versions of the application did not put multiple instances of the dialog box on the screen at once. If an attacker placed a deny ACE on the object, the user would not see any jokes—not a major problem!

At an absolute minimum, you should create an ACL that does not allow all users to

  • Write a new DACL to the object [Everyone (WRITE_DAC)]

  • Write a new owner to the object [Everyone (WRITE_OWNER)]

  • Delete the object [Everyone (DELETE)]

The access mask will vary from object to object. For example, for a registry key, the mask will be the following:

DWORD dwFlags = KEY_ALL_ACCESS 
                & ~WRITE_DAC 
                & ~WRITE_OWNER
                & ~DELETE;

For a file or directory, it will be like this:

DWORD dwFlags = FILE_ALL_ACCESS 
                & ~WRITE_DAC 
                & ~WRITE_OWNER
                & ~DELETE
                & ~FILE_DELETE_CHILD 

Other Access Control Mechanisms

Using ACLs is a useful method to protect resources, but there are other ways too. Three of the most common are .NET Framework roles, COM+ roles, IP restrictions, and SQL triggers and permissions. What makes these a little different from ACLs is that they are built into specific applications and ACLs are a critical core component of the operating system.

Roles are often used in financial or business applications to enforce policy. For example, an application might impose limits on the size of the transaction being processed, depending on whether the user making the request is a member of a specified role. Clerks might have authorization to process transactions that are less than a specified threshold, supervisors might have a higher limit, and vice presidents might have a still higher limit (or no limit at all). Role-based security can also be used when an application requires multiple approvals to complete an action. Such a case might be a purchasing system in which any employee can generate a purchase request but only a purchasing agent can convert that request into a purchase order that can be sent to a supplier.

The definition of "roles" is typically application-specific, as are the conditions under which one is willing to authorize specific actions.

Let’s look at two programmatic role mechanisms supported by Windows: .NET Framework Roles and COM+ Roles.

.NET Framework Roles

.NET Framework role-based security supports authorization by making information about the principal, which is constructed from an associated identity, available to the current thread. The identity (and the principal it helps to define) can be either based on a Windows account or be a custom identity unrelated to a Windows account. .NET Framework applications can make authorization decisions based on the principal’s identity or role membership, or both. A role is a named set of principals that have the same privileges with respect to security (such as a teller or a manager). A principal can be a member of one or more roles. Therefore, applications can use role membership to determine whether a principal is authorized to perform a requested action.

More Information

A full explanation of .NET Framework roles is beyond the scope of this book. I recommend you refer to one of the books in the bibliography (such as Lippert or LaMacchia, Lange, et al) for more information.

To provide ease of use and consistency with code access security, .NET Framework role-based security provides PrincipalPermission objects that enable the common language runtime to perform authorization in a way that is similar to code access security checks. The PrincipalPermission class represents the identity or role that the principal must match and is compatible with both declarative and imperative security checks. You can also access a principal’s identity information directly and perform role and identity checks in your code when needed.

The following code snippet shows how you can apply .NET Framework roles in a Web service or a Web page:

WindowsPrincipal wp = (HttpContext.Current.User as WindowsPrincipal);

if ( wp.IsInRole("Managers")) {

  //User is authorized to perform manager-specific functionality

}

You can perform a similar task on the current thread:

WindowsPrincipal principal = 
    (Thread.CurrentPrincipal as WindowsPrincipal);
if (principal.IsInRole("Administrator")) {
        //user is an admin
}

Note that WindowsPrincipal.IsInRole verifies that the caller is a member of a Windows group, and GenericPrincipal.IsInRole determines whether the caller is a member of a generic role, where the role population may come from a database or a configuration file. The GenericPrincipal constructor allows you to define the principal’s role membership. The following C# example outlines the process.

GenericIdentity id = new GenericIdentity("Blake");
//Role list could come from an XML file or database
String[] roles = {"Manager", "Teller"}; 
GenericPrincipal principal = new GenericPrincipal(id, roles);

COM+ Roles

COM+ roles are somewhat similar to Windows groups, but rather than being defined and populated by a network administrator, they are defined by the application designer at development time and populated by an application administrator at deployment time. This allows for great flexibility because the network group membership and the application role membership are related yet independent, which allows for application design flexibility.

Roles are enforced by COM+ at the application level by using the Component Services management tool, or they can be enforced programmatically using the IsCallerInRole method. The following Visual Basic code shows how the method is used:

‘ Get the security call context.
Dim fAllowed As Boolean
Dim objCallCtx As SecurityCallContext
Set objCallCtx = GetSecurityCallContext()

‘ Perform the role check.
fAllowed = objCallCtx.IsCallerInRole("Doctor")
If (fAllowed) Then
    ’ Act according to the result. 
End If

Unlike ACLs, which protect resources, roles protect code. It is the code that then accesses the resource being protected. However, role-enforcing code can combine other business rules with the role logic to determine access. The following code highlights this.

fIsDoctor = objCallCtx.IsCallerInRole("Doctor")
fIsOnDuty = IsCurrentlyOnDuty(szPersonID)
If (fIsDoctor And fIsOnDuty) Then
    ’ Perform tasks that require an on-duty doctor.
End If

The combination of business logic and role-based authorization is a powerful and useful capability.

IP Restrictions

IP restrictions are a component of most Web servers, including IIS. Using IP restrictions, a developer or administrator can restrict access to parts of a Web site to specific IP addresses (for example, 192.168.19.23), subnets (192.168.19.0/24), DNS names (http://www.microsoft.com), and domain names (*.microsoft.com). If you’re building Web-based applications, don’t rule out using IP restrictions. For example, you might include some form of administration functionality. One way of restricting who can use the administration tools is to place an IP restriction limiting the usage to the IP addresses of certain administration machines.

If you find your analysis of your business requirements and access rights includes wording like "accessible only at the local machine" or "deny access to all users and computers in the accounting.northwindtraders.com domain," you might need to consider using IP restrictions.

IP restrictions can also be useful if you include functionality that you want enabled by default but don’t want attackers using. You can achieve this by setting an IP restriction on the virtual directory you create to allow your code to execute only at the local machine (127.0.0.1).

Important

If you want to enable potentially vulnerable Web-based functionality by default, consider setting an IP restriction that allows the code to execute from 127.0.0.1 only.

The following sample VBScript code shows how to set IP restrictions on the Samples virtual directory on the default Web server such that only localhost (that is, the reserved address 127.0.0.1) can access it:

‘ Get the IP Settings.
Dim oVDir
Dim oIP
Set oVDir = GetObject("IIS://localhost/W3SVC/1/Samples")
Set oIP = oVDir.IPSecurity

‘ Set the IP grant list to 127.0.0.1.
Dim IPList(1)
IPList(1) = "127.0.0.1"
oIP.IPGrant = IPList

‘ Do not grant access by default.
oIP.GrantByDefault = False

‘ Write the information back to 
‘ Internet Information Services, and clean up. 
oVDir.IPSecurity = oIP
oVDir.SetInfo
Set oIP = Nothing
Set oVDir = Nothing

SQL Server Triggers and Permissions

SQL Server triggers allow the developer to place arbitrarily complex access rules on SQL tables. A trigger is called automatically by the SQL engine when data in the table is either added, deleted, or modified. Note that triggers are not used when data is read. This can be problematic, as you might create an application with some access control logic using one or more triggers to access control logic in other parts of the database, such as permissions. The triggers will not be executed if a read operation is attempted.

Permissions are to SQL Server what ACLs are to Windows and are in the simple form "subject doing something to object." Examples include "Blake can read from the Accounts table" and "Auditors can Read, Write, and Delete from the AuditLog table." All objects can be secured in SQL Server by using permissions.

A Medical Example

Let’s look at an example that uses other access control techniques. This is a simplified scenario from a medical application. Interviews with the client reveal the following scenario when a doctor updates a patient’s medical records:

Upon consultation, the doctor searches for, reads, and then updates the patient’s medical information with the new findings, and an audit entry is written to the audit log. Nurses, charge nurses, and doctors can read a patient’s medicine record, and charge nurses and doctors can update the patient’s medicines. Any access to the patient’s medicines is also audited. Only auditors can read the audit log, and doctors should never be auditors and therefore should never read from the log nor update the log.

It is determined in this case that search is the same as read.

From this we derive the following access policy for the patient data:

  • Doctors (Read, Update)

The following is the access policy for the patient’s medicine data:

  • Doctors (Read, Update)

  • Charge Nurses (Read, Update)

  • Nurses (Read)

And the following access policy is derived for the audit log:

  • Doctors (Deny Read, Deny Update)

  • Auditors (All Access)

  • Everyone (Write)

In this example, charge nurses, doctors, and auditors can be Windows groups or SQL Server or COM+ roles. (Note that other medical scenarios might change the access permissions.) It’s important to realize that the resources should not be implemented as resources that can be ACLed. A good example is the data held in SQL Server—in this case, all patient data is held in the database, as is the audit log.

The nice thing about this scenario-based approach is that the access control policy is implementation-independent. For example, you might determine that a trigger on a SQL Server table determines the implementation of that policy. The following is an example of a trigger that is fired when a user attempts to update or delete data in an audit log table. If that user is not in the Auditor’s group, the transaction is rolled back (that is, the transaction does not occur):

create trigger checkaudit on tblAuditLog
for update, delete
as 
begin
if not is_member(‘NorthwindtradersAuditors’)
    rollback tran
end

Note that the trigger is not called when anyone inserts data into the audit log, and according to the business rules anyone can write to the log. There is a flaw, however: anyone can read the data from the audit log, and triggers are not used when reading data. In this case, you’d be wise to apply a permission to the table also, such as "public can only write to the audit log." Public is the equivalent of the Everyone group in Windows. Because audit logs are so sensitive, it’s worthwhile having two levels of protection. Remember: defense in depth! In this case, the permissions on the table and the trigger acting as a backstop in case an administrator accidentally removes the permissions from the audit log table provide defense in depth.

An Important Note About Access Control Mechanisms

Access control mechanisms that are not built into the operating system might lead to vulnerabilities in the application. Allow me to explain. Take a look at Figure 6-3, which shows a system protecting resources with IP restrictions implemented in a Web server.

A system protecting resources with IP restrictions.

Figure 6-3. A system protecting resources with IP restrictions.

The problem in this example is that the system also has file access capabilities enabled. If the attacker can use the file services directly, he might be able to bypass the IP restrictions because IP restrictions, a Web server feature, don’t exist in the file access services.

Important

When designing access control mechanisms, you need to be careful that the system cannot be circumvented by other means.

Here’s an example. While I worked on the IIS team, another team created a Web site to invite their entire team to a private screening of Star Wars, Episode One: The Phantom Menace. We in IIS believed we should be invited too, so a small group of us decided to invite ourselves! As we probed the Web server, we noted that the logic determining whether a user was on the other team was held in the Web site’s pages. A little more work showed the team had a file share (an SMB share) to the Web pages. We connected to the file share, and sure enough the ACLs on the Web site files were weak. So we changed the site to invite IIS to the movie also!

Important

You can provide access control to your application in many ways, including ACLs, SQL permissions, IP restrictions, and roles. Make sure you use the most appropriate technology for your application, and in some instances be prepared to layer the technologies—for example, using both ACLs and IP restrictions—in case one layer is compromised or incorrectly set up by the administrator.

Summary

With the possible exception of encryption, ACLs are a persistent object’s last line of defense from attack. A good ACL can mean the difference between a secured object and a compromised network. Remember the principle of defense in depth discussed in Chapter 3, and use ACLs to provide a valuable and effective layered defense.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset