Export (0) Print
Expand All

Security and Application Development in SharePoint: First Steps

Published: April 9, 2008

By Reza Alirezaei , Microsoft Office SharePoint Server 2007 MVP

See other Security MVP Article of the Month columns.

Introduction

With the release of Windows SharePoint Services (WSS) 3.0 and Microsoft Office SharePoint Server (MOSS) 2007, SharePoint Web application are now among the many computing services that can be exposed to the Internet, and thus they pose an inviting target to intruders. SharePoint goes a long way toward providing a robust security model right out of the box, and it also allows organizations to maintain their own sets of compliance requirements and information security policies. Although the SharePoint platform is secure, you are still responsible for making your applications secure. You must follow security best practices throughout the design and development stages of your business solutions built on the top of this great platform.

Security principles in the world of programming using the SharePoint object model usually boil down to two key principles at design and development stages:

  • Know your threat model.

  • Know what security context your code runs on behalf of.

The truth is that if you don't know these two principles, you won't be able to detect and stop potential security breaches or even follow other code-level security best practices, such as coding for a partially trusted environment.

Threat Model Analysis

Simply put, thread model analysis is just taking the architect or developer hat off and putting the attacker hat on. This allows you discover potential holes in your application that a malicious user or process may exploit to compromise your environment. I have a security checklist called "STRIDE" that I always keep in mind when designing or developing SharePoint and non-SharePoint projects. The STRIDE acronym stands for:

  • Spoofing

  • Tampering

  • Repudiation

  • Information disclosure

  • Denial of service

  • Elevation of privilege

I know exactly how my application works internally, so in testing for security I try to foil my application by directly compromising it! Yes, I simply attempt to hack my own application and find the security holes before others do it for me. There are many tools that can help you to take this penetration exercise to the next level and assess more sophisticated results, but, believe me, you can always perform the basic testing yourself, which is far better than no testing at all.

I won't go into the details of each item in the STRIDE checklist here, but instead explain most of them in a context where it makes more sense. Please bear with me and keep these terms in mind until they are discussed.

Identity and Principal Objects at a Glance

An identity object represents a specific user, and it is constructed during the authentication process usually in a form of Windows or Forms authority. A principal object, on the other hand, is more than just an identity: it represents an identity and its roles, which can be used to make security decisions by the operating system, the WSS authorization engine, or even your code.

Establishing a proven identity and including it in further requests is an important step, because authorization checks to various resources (inside and outside of WSS runtime) uses that identity for evaluating access controls. Accessing securable SharePoint objects, WFE and application servers' file systems, back-end servers running Microsoft SQL Server, and registry are all perfect examples of the resources that the WSS engine or your code may request to access.

First, you need to understand and differentiate between different identities and principal objects that you may encounter in a SharePoint context. As illustrated in the table below, there are three different identity types:

Table 1

Now that you know how to obtain the current identity or construct a new one based on the authentication type, let's have a look at different ways you can acquire principal objects:

  • Thread.CurrentPrincipal: Returns the principal object of the currently executing CLR thread on the top of Windows thread.

  • HttpContext.Current.User: Returns the principal object of the current Web request.

  • SPContext.Current.Web.CurrentUser: Returns an SPUser object that represents the current WSS user.

When a user or group is added to a site, that user or group is just a principal represented by an object of type SPPrincipal. This object is also assigned permissions to control security within the WSS runtime. An SPPrincipal object can represent one of the following types of users or groups:

  • Windows user (SPUser)

  • Windows group (SPGroup)

  • FBA user (SPUser)

  • FBA role (SPGroup)

  • SharePoint groups (a collection of SPGroup objects)

Only two things need to be highlighted here:

  1. First, both SPUser and SPGroup inherit SPPrincipal.

  2. Second, Roles property of SPPrincipal object is now obsolete, so is Roles property of SPUser and SPGroup objects. This means that you shouldn't obtain the roles associated with SPUser and SPGroup objects using this property. A new authorization object model is implemented via SPRoleDefinition and SPRoleAssignment classes to help you define new roles, obtain existing roles for a securable object, and assign users and groups to roles. For more information, see Changes in the Authorization Object Model.

Different Accounts

There are five different accounts that you may encounter while interacting with your SharePoint environment:

  1. Local System account

  2. Network Service account

  3. Local Service account

  4. Custom Domain account

  5. System account (inside WSS runtime only)

Back in the old days, prior to the release of Microsoft Windows 2000 Server, the Local System account could not authenticate across the network, therefore a custom domain account was the only option services could use for authentication in order to access resources across the machine boundary. Later, the Local System account was improved to allow authentication to network resources by using the computer's credentials. With the release of the Microsoft Windows Server 2003, two new built-in account types were introduced: the Network Service account and the Local Service account.

When authenticating remotely, the Network Service account also uses the computer's credentials with the least possible privileges on the server itself. The Network Service account was a brilliant idea because in case your application is compromised, at least you have limited the potential damages. However, the Network Service account does introduce some other limitations. Network Service account is a general purpose account, so in a shared environment where multiple SharePoint Web applications are using this account, any changes to the access control and permissions associated with this account in favor of one SharePoint Web application might affect other applications. The bottom line is that the Network Service account doesn't give you that desirable isolation that will be discussed in the next paragraph. Just like the Network Service, the Local Service account is also an account with reduced privileges, but with one difference: it cannot authenticate to network resources. It can only touch local resources like local databases or local file system.

Among all these account types, Custom Domain accounts are the most common in SharePoint farm installations. By following organizational policies and procedures, administrators often create Custom Domain accounts with no special permissions. Subsequently, they create or extend a Web application so that its application pool identity is set to that Custom Domain account. The beauty of this process is that creating a Web application sets all the necessary database-level permissions for the Custom Domain account. Add the custom domain account to IIS_WPG (IIS Worker Process Group) and WSS_WPG (read access to system resources used by WSS 3.0) and you're all set. I created a custom application pool identity called LIWAREINC\securitydemosvc that I will use throughout this paper. It literally took me less than five minutes to set up this account (LIWAREINC\securitydemosvc), create a new app pool and finally create a SharePoint Web application that runs under the new app pool.

There is an important rule to remember: Isolate your application pool. By doing so, you can reduce the risk of your environment being compromised, or if one app pool goes down for whatever reason, it doesn't affect all the rest. It is absolutely okay if you decide to pool a couple of applications together—for example, in order not to reach the hard limit in the total number of app pools you can use in a farm roll out before you considerably degrade the performance—as long as they are not pooled with SSP or Central Administration sites. Shared Services Provider and Central Administration sites should always get their own application pools and identities different from the rest of your Web applications exposed to the end users.

One more account is left from the list above: System account, which is only visible to the WSS runtime. If you have ever seen "Welcome System Account" on the top right corner of a site's default master page, this means that you either used your own account as the application pool identity or you used the application pool identity to log in. In either case, it is a big no no. As I alluded to earlier, creating a process identity has become a lot less painful in WSS 3.0 and MOSS 2007, so there is not really a reason for being lazy anymore! The System account is a clone of the current application pool identity. There are two main advantages to this cloning process. First, "System Account" login name always remains the same and is encapsulated from the underlying application identity (You may change App Pool Identity for whatever reason). Second, the real login name of the application pool identity is never revealed, which is a good security precaution.

In case you still want to log in to your site using the application pool identity (although, honestly, I can't think of the reason) without “System Account” being shown in the welcome user control or to hide an account by showing it as "System Account," here are the steps you need to follow:

  1. Go to Central Administration.

  2. Browse to Application Management.

  3. Click Policy for Web Application.

  4. Select your Web app.

  5. Click your account in the list.

  6. In the Edit Users page, clear the Account operates as System checkbox.

Figure 1

Figure 1. Running an account as system

Web.config

From the Internet Information Services (IIS) standpoint, once you create a SharePoint Web application (or when you extend an existing application) the result is always the same: An IIS Web site gets created. Each generated IIS site comes with a web.config file containing configurations about how your SharePoint application should behave. There are two parts of a fresh web.config file that matter the most for our discussion here.

Table 2

In the event you flip the authentication type of your SharePoint Web application from Windows to Forms, and whether or not the "Enable anonymous access" checkbox is selected in the Authentication Provider page in the Central Administration site, the above settings will change as follows:

Table 3

Security Note: No matter how much efforts you put in writing secure code, your SharePoint Web application can still be vulnerable to attack if you neglect to secure the Web.config files. This is mainly because many configuration settings stored in the Web.config files are not secured by default.
If you do store sensitive information such as connection strings (for example for an authentication provider) or those included in <appSettings> node, use inherent ASP.NET 2.0 encryption capabilities to prevent information disclosure. ASP.NET 2.0 makes it extremely easy to encrypt any section of your Web.config file via command prompt (aspnet_regiis) or programmatically, in case your code resides in a shared environment where you have no console access to the Web.config file. The beauty of the new security features is that you don't need to decrypt the encrypted configuration settings for use by your code. ASP.NET does all the magic for you, so you can't really go wrong! Remember, the best defense against information disclosure is to ensure that either valuable information are not disclosed or at least you have made it hard enough for an attacker to have access to potentially sensitive data and information.
For securing the Web.config files, the same rules that apply to an ASP.NET Web application apply to a SharePoint Web applications as well. That being said, I will skip a detailed explanation here. For more information see Walkthrough: Encrypting Configuration Information Using Protected Configuration.

Web Parts and Custom Application Pages

I would like to start with Web parts and application pages for two major reasons. First, they are the most common development tasks you perform. Second, they return exactly the same results with regard to the current security context. To show the current security context in a Web part, we need to write a class that derives from WebPart class and override RenderControl event as shown below:

public class ShowIdenWP: WebPart {
    public override void RenderControl(HtmlTextWriter writer)
    {
        base.RenderControl(writer);
        writer.Write("<br><b>WindowsIdentity:</b>" + WindowsIdentity.GetCurrent().Name);
        writer.Write("<br><b>HttpContext:</b>" + HttpContext.Current.User.Identity.Name);
        writer.Write("<br><b>CLR Thread:</b>" + Thread.CurrentPrincipal.Identity.Name);
        writer.WriteLine("<br><b>WSS User:</b>" + SPContext.Current.Web.CurrentUser.Name);        
    }

In the code snippet above, there is one thing that needs to be highlighted: In case you have enabled anonymous access for your site, the last statement (WSS User) will throw "Object reference not set to an instance of an object error." This makes sense because there is no WSS context associated with anonymous users. Either comment that line or temporarily disable anonymous access while you are testing the code snippet.

With our Web part properly coded to show WindowsIdentity, HttpContext, CLR Thread, and WSS User, let's just deploy it to the server and finally place it on the home page. As shown in the following pictures, when a SharePoint site is protected using Windows authentication, SPUser and Windows Identity are the same, but in the FBA protected site, things are a little bit different in that WindowsIdentity doesn't match SPUser.

Figure 2

Figure 2. Web Part in a Windows Zone

Figure 3

Figure 3. Web Part in an FBA Zone

Here's where things get a bit funky. Figure 3 shows SPUser points to "Will Johnston," whereas WindowsIdentity resolves in an underlying Windows account defined in IIS Authentication Methods dialog box, in this case, IUSER_LITWAREDEMO. Unlike the Windows user, the FBA user context is not associated with a Windows Principal. This means that whatever resources "Will Johnston" requests within WSS, access control is evaluated based on the SPPrincipal object (Will Johnston), but the minute his request goes beyond the WSS boundary, the IUSER_LITWAREDEMO account represents him for operating system–level authorization checks.

To assess the security context for application pages, first we need to create an aspx page using the following code snippet.

SecurityDemoAppPage.aspx:

<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Assembly Name="securityDemoApplicationPage, Version=1.0.0.0, Culture=neutral, 
PublicKeyToken=4872ca240a30775e" %>

<%@ Page Language="C#" MasterPageFile="~/_layouts/application.master" 
         Inherits="securityDemoApplicationPage.SecurityDemoAppPage"
         EnableViewState="false" EnableViewStateMac="false" %>


<asp:Content ID="PageTitle" runat="server" contentplaceholderid="PlaceHolderPageTitle" >
Security Demo : Application Page
</asp:Content>

<asp:Content ID="PageTitleInTitleArea" runat="server" contentplaceholderid="PlaceHolderPageTitleInTitleArea" >
Security Demo : Application Page
</asp:Content>

<asp:Content ID="Main" contentplaceholderid="PlaceHolderMain" runat="server"> 
<asp:Literal ID="LiteralMain" runat="server" /> 
</asp:Content>

For an aspx page to be able to live in a LAYOUTS directory while using code-behind technique, we need to create a class that inherits from Microsoft.SharePoint.WebControls.LayoutsPageBase. Next, we need to override the following method to show current security context, just like what we did for the Web part example above.

SecurityDemoAppPage.cs:

public class SecurityDemoAppPage : LayoutsPageBase 
{

      protected Literal LiteralMain;
      protected override void OnLoad(EventArgs e) 
   {
        StringBuilder mainContent = new StringBuilder();
        mainContent.Append("<br><b>WindowsIdentity:</b>" + WindowsIdentity.GetCurrent().Name);
        mainContent.Append("<br><b>HttpContext:</b>" + HttpContext.Current.User.Identity.Name);
        mainContent.Append("<br><b>CLR Thread:</b>" + Thread.CurrentPrincipal.Identity.Name);
        mainContent.Append("<br><b>WSS User:</b>" + SPContext.Current.Web.CurrentUser.Name);
        LiteralMain.Text = mainContent.ToString();

    }
}

Finally, we need to place the aspx page in the LAYOUTS folder and add the assembly containing the code behind to either the global assembly cache (GAC) or bin folder of a SharePoint Web application. Next, let's just browse to the SecurityDemoAppPage.aspx page from both FBA and Windows zones and observe the results.

Figure 4

Figure 4. Application Page in a Windows Zone

Figure 5

Figure 5. Application Page in an FBA Zone

Notice the results are identical to what we experienced earlier for our security demo Web part. You will also get the same results in your feature receivers (FeatureActivated, FeatureDeactivating), user controls, HttpHandler, HttpModule, delegate, and server controls, so there is no point to demonstrating them as well. Please see the summary table at the end of this paper for more information.

By placing your assembly in global assembly cache, you are bypassing Code Access Security (CAS) restrictions on your code and, therefore, lowering the in-depth defense measures of your application. Although strict code access security (Read it overCASing) might not be the answer to every SharePoint project you do, it is a highly recommended security precaution for some scenarios such as in a shared environment where maximum lock down is required. Considering that Web parts impersonate the security context of the caller, putting a Web part assembly that you don't trust in GAC, and running it in full trust, is not a desirable security state. It is simply a suicide.

Speaking of whether to choose bin or GAC, I would always test my code for deployment to both bin and GAC and based on what I develop and each project's security requirement, I decide where to deploy it. There is no such a rule forcing you to choose bin or GAC folder as the only destination for all of your assemblies.

As yet another important rule, if you are writing the response manually or if your code somehow participates in sending HTTP response data back to a client (like RenderWebPart method in a Web part), then you have to HTML-encode it to turn dangerous HTML tags into more secure escape characters. This becomes a serious security precaution when the output is determined to contain input that you don't trust, such as input obtained from a Web part property. Fortunately, SPEncode class, which is designed to encode a larger set of characters for SharePoint use, is there to help you. It has a method called HTMLEncode that can be used to send data safely back to a client computer. Following is an example of using the HTMLEncode method to encode data obtained from Text property of a Web part.

protected override void RenderWebPart(HtmlTextWriter output)
{
   output.Write("My custom text  is: <b>" + SPEncode.HTMLEncode(this.Text) 
      + "</b>");
}

Event Handlers

Event handlers (one of the custom integration points to the WSS runtime) are a great way to hook up managed code to many events at lists, document libraries, and some limited events at Web or site collection levels. There are two major types of events that I would like to cover in this section: synchronous events such as ItemAdding, and asynchronous events such as ItemAdded. First, let's create a list called "Security Demo List" to bind our event handler.

Figure 6

Figure 6. Security Demo List to host the event handler

At this point, we need to code our event handler. Below are two events for which we want to examine the security context. In both events, the Task list of the current Web site is accessed and a task item is created with its description set to the WindowsIdentity of the current security context. Obviously, I could use the list item itself for persisting such information. To keep things simple and in sync with other parts of this paper, I decided to use the Task list instead.

public class securityDemoListItemEventReceiver : SPItemEventReceiver
    {
        public securityDemoListItemEventReceiver(){}

        public override void ItemAdded(SPItemEventProperties properties)
        {
            SPWeb parentWeb = properties.ListItem.Web;
            SPList taskList = parentWeb.Lists["Tasks"];                          
            SPListItem itemAddedTaskItem = taskList.Items.Add();
            itemAddedTaskItem["Title"] = "ItemAdded event(Asynchronouse)";
            itemAddedTaskItem["Description"] = "WindowsIdentity:" + WindowsIdentity.GetCurrent().Name;
            itemAddedTaskItem.Update();               
        }

        public override void ItemAdding(SPItemEventProperties properties)
        {
            SPWeb parentWeb = properties.OpenWeb();
            SPList taskList = parentWeb.Lists["Tasks"];
            SPListItem itemAddingTaskItem = taskList.Items.Add();
            itemAddingTaskItem["Title"] = "ItemAdding event(Synchronouse)";
            itemAddingTaskItem["Description"] = "WindowsIdentity:" + WindowsIdentity.GetCurrent().Name;
            itemAddingTaskItem.Update();
        }
	// other overriden methods of your event handler   
    }

Once the event handler is deployed and attached to a list, we are ready to trigger ItemAdded and ItemAdding events from two different entry points. Barbara Decker (our Windows woman) adds an item from the Windows zone and Will Johnston (our FBA man) adds the second item from the FBA zone. Here is how the list looks like after the items are added.

Figure 7

Figure 7. Two items are added to the list by the Windows and FBA users

From the pictures below you can tell that the results are interesting. The ItemAdded event and ItemAdding event both ran under the security context of the Windows user who triggered them by adding an item to the list. However, as we predicted before, things are a little bit different on the FBA side. The ItemAdding event was executed under LITWAREINC\IUSR_LITWAREDEMO account, but the ItemAdded event (an asynchronous event) ran under the security context of LITWAREINC\securitydemosvc (application pool identity).

Figure 8

Figure 8. ItemAdded and ItemAdding events add task items to the task list with description set to the current Windows Identity

Security Note: Asynchronous events in FBA zone that run under the security context of the application pool identity can lead you to a denial of service (DoS) vulnerability in event handlers. This security hole allows an attacker who successfully exploits it to affect your site in a number of ways. As an old security saying goes: By default, all input is evil! This is especially true when you receive input from remote anonymous clients. In case you have opened up a list to anonymous users and you use their input to perform some operations against other systems in the event handler, I strongly suggest that you put an input validation in place to prevent SQL injection, buffer overflow, and Cross-site scripting (XSS).

Another scenario to be careful about is when you launch a long-running operation once an item is added to the list. I'm sure that you can imagine the effect on the site's response time, availability and performance should an attacker add thousands of items to the list and your long-running operation kicks in every time “ItemAdded event” fires. The big question is, "Have you seen these vulnerabilities in your threat modeling analysis?"

Timer Jobs

Timer jobs are a great way of sending notifications, off-loading long-running operations or tasks that need to be executed on scheduled basis. Both WSS 3.0 and MOSS 2007 use quite a few timer jobs to perform various internal tasks such as content deployment, clean up and etc. Although, Central Administration gives the ability to see job definitions and their status, a timer job gets kicked off outside the context of Windows SharePoint Services (or, better to say, outside its HttpContext). A timer job gets executed by Windows SharePoint Services Timer service (OWSTIMER.EXE); therefore, its security context is whatever identity this service is set to log on. If you open Services snap-in, you can find this service and find out about the logon account. In this case, it is NT AUTHORITY\Network Service.

Figure 9

Figure 9. Windows SharePoint Services Timer Service runs as NT AUTHRORITY\NetworkService

Alternatively, when running, the timer service executable shows up in the Task Manager.

Figure 10

Figure 10. WSS Timer service executable

A custom timer job consists of two main parts:

  1. Job Definition Class: A class that is derived from SPJobDefinition and implements the logic of your job.

  2. Launcher: Your code that creates an instance of the job definition and sets its schedule for execution. This can be done in a feature receiver, custom application page, or console application.

Let's get our timer job definition coded with our routine statements to show the current security context.

public class ShowSecurityContext : SPJobDefinition
    {     
        public ShowSecurityContext(): base(){}

        public ShowSecurityContext(string jobName, SPService service, SPServer server, SPJobLockType
        targetType) : base(jobName, service, server, targetType){}

        public ShowSecurityContext(string jobName, SPWebApplication webApplication)
            : base(jobName, webApplication, null, SPJobLockType.ContentDatabase)
        {
            this.Title = "Secuirty Demo Timer Job";
        }
        
        public override void Execute(Guid contentDbId)
        {
            SPWebApplication webApp = this.Parent as SPWebApplication;
            SPWeb parentWeb = webApp.Sites[0].OpenWeb();
            SPList taskList = parentWeb.Lists["Tasks"];
            SPListItem itemAddedTaskItem = taskList.Items.Add();
            itemAddedTaskItem["Title"] = "Timer Job Security Context";
            itemAddedTaskItem["Description"] = "WindowsIdentity:" + WindowsIdentity.GetCurrent().Name;
            itemAddedTaskItem.Update();
                                  
        }
    }

Next, we need a mechanism to instantiate and schedule this job. To keep things simpler, I am going to instantiate this job using a console application.

class Program
    {
        static void Main(string[] args)
        {
            SPWebApplication webApp = SPWebApplication.Lookup(new Uri("http://litwaredemo"));
            SPWeb web = webApp.Sites[0].OpenWeb();
            ShowSecurityContext SecurityDemoJob = new ShowSecurityContext("Secuirty Demo Timer
            Job",webApp);
            SPOneTimeSchedule schedule = new SPOneTimeSchedule();
            schedule.Time = DateTime.Now;
            SecurityDemoJob.Schedule = schedule;
            SecurityDemoJob.Update();
        }
    }

Go ahead and run this console application. This job is defined to be executed once and its execution time is set to DateTime. Now, you may miss the opportunity to see its status in the Timer job status page in the Central Administration site, but if you go to the Tasks list you will find the following task.

Figure 11

Figure 11. Task item that is created as the result of timer job execution

Developers often assume that because a timer job doesn't run within the context of Windows SharePoint Services, they won't be able to access many Web-related settings for the Web application the job is registered with. Well, in fact, that's a common misconception. In Execute method of your timer job definition, you are always passed the pointer (GUID) to the Content Database or you can access the parent Web application using the following statement:

SPWebApplication webApp = this.Parent as SPWebApplication;

From here you have two avenues to take: SharePoint objects and IIS settings. From SPWebApplication object, you can get an SPIisSettings object representing the IIS settings for a particular URL zone.

SPIisSettings settings = webApp.IisSettings[SPUrlZone.Internet];

Unfortunately, SPIisSettings class only offers access to some parts of configuration files such MembershipProvider or AllowAnonymous, but nothing stops you from combining it with ASP.NET 2.0 configuration API (WebConfigurationManager Class) to dig further into many more configuration settings in your Web.config files.

From the SharePoint perspective, once you have a hand on SPWebApplication, you have access to almost everything—such as SPSite, SPWeb, and SPList—but there is one thing that you don't have access to: the security context of the user who initiated the job! Why do I care? I'm glad you asked. Back to the first paragraph of "Identities and Principal Objects at a Glance." I mentioned that a principal object represents an identity and its roles which can be used to make security decisions by the operating system, the WSS authorization engine, or even your code. Yes, your code sometimes need to make security decisions, and here is where this all make sense. In case you need to pass any metadata from launcher to the instance of the running job, SPJobDefinition class maintains a property bag object (of type Hashtable) that can be used to store information while provisioning and that can be retrieved later when the job is executed. Below is an example of how you can pass information from the launcher to the running instance of your job—in this case, the current user's login name.

SecurityDemoJob.Properties.Add("CurrentUser", SPContext.Current.Web.CurrentUser.LoginName);

Here is how the changed timer job definition class looks:

public override void Execute(Guid contentDbId)
        {
            SPWebApplication webApp = this.Parent as SPWebApplication;
            SPWeb parentWeb = webApp.Sites[0].OpenWeb();
            SPUser user = web.AllUsers[this.Properties["CurrentUser "].ToString()];
           // Now you have the WSS user context who initiated the job
            ......                      
        }

Workflows

By design, workflows run with whatever privileges App Pool has within the WSS runtime, on the machine itself or across the machine boundary. This elevation in privilege helps workflows to accomplish tasks for which users might not have the required permissions, for example, adding a user to the site's visitor group upon approving the user’s request to join the site or when a document must be archived when it reaches its expiration date according to the document library's Information Management Policy. With a short overview of how out of the box security context works in workflows, now it is time to observe things in action.

Let's go ahead and build a simple sequential workflow. This workflow only creates a task and assigns it to LITWAREINC\administrator with the security context of the workflow shown in the task description. Once the task is marked as completed by the administrator, an e-mail message is sent to the administrator reporting the completion of workflow and the security context under which e-mail was sent. We will run this workflow automatically and manually to access the results in both cases.

Here are the activities of the workflow shown on the designer canvas.

Figure 12

Figure 12. Workflow designer canvas

At this point, we need to code different activities of our workflow. As you can see in the workflow code below, I have included a bunch of statements to log the security context as the flow moves between different activities in the workflow. Here is the partial class that supports our workflow logic:

public sealed partial class securityDemoWF: SequentialWorkflowActivity
	{
        public securityDemoWF()
		{
			InitializeComponent();
		}

        public Guid workflowId = default(System.Guid);
        public Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties workflowProperties = 
        	new Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties();
        public Guid taskID = default(System.Guid);
        public Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties taskProps = 
        	new Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties();
        public Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties afterProps = 
        	new Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties();
        public Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties beforeProps = 
        	new Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties();


        private void onWorkflowActivated(object sender, ExternalDataEventArgs e)
        {
            workflowId = workflowProperties.WorkflowId; //initialize the id
        }

        private void createTask(object sender, EventArgs e)
        {
            taskID = Guid.NewGuid(); //initialize the task id
            taskProps.Title = "Security Demo";
            taskProps.AssignedTo = "LITWAREINC\\administrator";
            taskProps.Description = "WindowsIdentity:" + WindowsIdentity.GetCurrent().Name;
        }

        bool isFinished = false;
        private void onTaskChanged(object sender, ExternalDataEventArgs e)
        {
            // Just set it to true to let the workflow flow!
            isFinished = true;
        }
        
        private void taskNotFinished(object sender, ConditionalEventArgs e)
        {
            e.Result = !isFinished;
        }

        public String histDescription = default(System.String);
        private void setHistoryStrings(object sender, EventArgs e)
        {
            histDescription = " Security context is: " + WindowsIdentity.GetCurrent().Name;
        }

        public String completeMailBody = default(System.String);
        public System.Collections.Specialized.StringDictionary completeMailHeader = 
        	new System.Collections.Specialized.StringDictionary();
        private void sendCompletedMail(object sender, EventArgs e)
        {
            completeMailHeader.Add("To","administrator@litwareinc.com" );
            completeMailHeader.Add("Subject", "Security summary of workflow");
            completeMailBody = "Security summary: <br><br>" +
                "WindowsIdentity:" + WindowsIdentity.GetCurrent().Name; ;
        }

	}

With the workflow completely coded and deployed, you can now go ahead and attach this workflow to a document library or a list.

Figure 13

Figure 13. Adding workflow to a document library

Barbara Decker uploads a document causing the workflow to be initiated.

Figure 14

Figure 14. Workflow in progress

Let's have a look at the task that is created and assigned to the administrator:

Figure 15

Figure 15. Security context of the workflow

Recall that workflow runs under the application pool identity— in this case, LIWAREINC\securitydemosvc. There are a few things worth mentioning about the results:

  1. Both workflow history item and the final e-mail message sent to the administrator indicate that the LITWAREINC\securityDemosvc was the process identity.

  2. Initiating the workflow automatically or manually will give the same result.

  3. Initiating the workflow from an FBA-protected site will give you the exact same result. No difference whatsoever.

The fact that out-of-the-box workflow always runs as an application pool identity cannot be overridden through UI. As such, it is up to the workflow developer and his or her code to detect the current user's security context and decide whether such actions must be performed or rolled back. There are only two ways to handle this:

  1. Detecting the originator's security context and running code on behalf it by mimicking their permissions (a.k.a. impersonation). Obviously, this may result in taking a pessimistic approach to deny actions due to the current user's lack of permission.

  2. Let the code use the built-in elevation of privilege.

Security Note: Spoofing, tampering, information disclosure, and elevation of privilege (from the STRIDE checklist discussed in the beginning of this paper) are quite relevant to the workflow discussion. These security principles become even more critical when Task list and History list are shared between multiple workflows in your site. Remember, Task and History lists are the lists that, by default, are created with no permission restrictions; thus, again by default, everyone can view information that may not be meant to be disclosed to begin with (a heaven for spoofers). Additionally, with no proper list-level or item-level permissions in place, such information can easily be tampered with for malicious purposes. These items can be secured in a number of ways:

  1. Create separate Task and History lists for your sensitive workflows and apply required list-level permissions to protect the information created and collected by your workflows. This step would, for example, prevent Barbara from finding out why the big boss has turned down her manager's request to promote Barbara in one of the "Promote Employee" workflow instances. Isolating Task and History lists should take care of information disclosure concerns.

  2. Modify the permissions on the Task and Workflow History lists to meet your requirements. For example, drop view rights from Workflow History list and limit the access to a certain group within your site.

  3. In your workflow code, you can set item-level permissions when creating tasks by using one of the following methods:

    1. Both CreateTask and CreateTaskWithContentType activities have a property called SpecialPermissions that takes a hashtable of key-value pairs of type string and SPRoleType. Setting the SpecialPermissions property in your code will strip out all existing permissions inherited from the parent list (Workflow Task List) and only adds permissions for each pair you added to the hashtable.

      private void createTask(object sender, EventArgs e)
              {
                  .......
                  CreateTask task1 = sender as CreateTask;             
                  HybridDictionary permsCollection = new HybridDictionary();
                  permsCollection.Add(taskProps.AssignedTo, SPRoleType.Administrator);
                  task1.SpecialPermissions = permsCollection;
      
                  
              }
    2. You can also code an OnTaskCreated event to use the object model to break the permission inheritance and assign your own permissions. Since the LogToHistoryList activity doesn't offer a SpecialPermissions property, this approach can also be used to tweak the permissions associated with the items you create in Workflow History list by default.

      private void OnTaskCreated (object sender, EventArgs e)
              {
      
      ........
      SPListItem listItem = workflowProperties.TaskList.Items.GetItemById(afterProps.TaskItemId);
       SPRoleDefinition roleDefinition = listItem.Web.RoleDefinitions.GetByType(SPRoleType.Contributor);
       SPUser user = listItem.Web.Users[taskProps.AssignedTo];
       SPRoleAssignment roleAssignment = new SPRoleAssignment(user.LoginName, user.Email, user.Name, user.Notes);
       roleAssignment.RoleDefinitionBindings.Add(roleDefinition);
       listItem.BreakRoleInheritance(false);
       listItem.RoleAssignments.Add(roleAssignment);
       listItem.Update();
            }
      
    3. If you use CreateTaskWithContentType, there is one more place that you can modify the permissions inherited from the Task list: The event handler that is bound to your task content type. In the ItemAdded method of your event handler, add the following code:

      DisableEventFiring();
        SPListItem listItem = properties.ListItem;
        listItem.BreakRoleInheritance(false);
        listItem.Update();
        listItem =SetItemLevelPermissions(listItem.Web, listItem, SPRoleType.Contributor,
               listItem[”Assigned To”].ToString());
        listItem.Update();
        EnableEventFiring();
      

      And here is the helper method:

      public static SPListItem SetItemLevelPermissions(SPWeb setSPWeb, SPListItem setListItem, SPRoleType setRoleType, string assignedTo)
      {
        int index = assignedTo.IndexOf(’;');
        int id = Int32.Parse(assignedTo.Substring(0, index));
        SPUser user = setSPWeb.SiteUsers.GetByID(id);
        SPRoleDefinition roleDefinition = setSPWeb.RoleDefinitions.GetByType(setRoleType);
        SPRoleAssignment roleAssignment = new SPRoleAssignment(user.LoginName, string.Empty, string.Empty, string.Empty);
        roleAssignment.RoleDefinitionBindings.Add(roleDefinition);
        setListItem.RoleAssignments.Add(roleAssignment);
        return setListItem;
      }
      

Note that in your real-life workflow projects, you often end up taking a hybrid approach where all or some of the techniques mentioned above are combined and used together. There is no such a thing as "One security solution that fits all your workflow projects."

Elevation of Privilege in Your Code

WSS 3.0 object model introduces a new programmatic technique called elevation of privilege. Basically, the new feature gives you the ability to run your code in an elevated security context (application pool identity). Below is one way of elevating the privilege in your code:

SPWeb  webInUserContext = SPContext.Current.Web;
                SPSite SiteInUserContext = SPContext.Current.Site;                
                Guid webGuid = webInUserContext.ID;
                Guid siteGuid = SiteInUserContext.ID;
            
	 // Scope of the RunWithElevatedPrivileges delegate starts here
                SPSecurity.RunWithElevatedPrivileges(delegate()
                {
                    // Reconstruct the SPSite object in the elevated context
                    using (SPSite site = new SPSite(siteGuid))
                    {

                        // Get the SPWeb object in the elevated  context
                        SPWeb web = site.OpenWeb(webGuid);
                   
	        // Do the work which requires higher privilege than current user here. 
	       
                       web.Dispose(); 
                    }

                });
                // Scope of the RunWithElevatedPrivileges delegate ends  here
			

Security Note: Using elevation of privilege comes with two obvious security warnings:

  1. Before leaving the elevated context, you must destroy the SPSite object (by using the Using Statement) and SPWeb object (explicit disposal), otherwise the context will remain elevated, so it is possible that the security is compromised. Apart from security, the unmanaged resources that the newly constructed SPWeb and SPSite objects hold (inside the elevated context) need to be released.

  2. Be careful what your code does in the elevated context, because it runs with full rights.

The inevitable question is whether some SharePoint developers misuse elevation of privilege for wrong security purposes or as a shortcut to bypass security. The answer is Yes! in case you weren't sure. We all need to learn security and work with it rather than always bypassing it.

What Else Is There?

Lots! I have only scratched the surface, but the good news is that for most of your SharePoint applications, what I have covered in this paper will give you a pretty good jump-start into what you need to consider as first steps from the security perspective. Probably, next steps for you to focus on include:

  • Making sure that your code runs with the least required access to resources in order to accomplish a task and no more (a.k.a. coding for a partially trusted environment).

  • Client Ticket Security in FBA scenarios.

  • Instrumentation to collect security-relevant information.

Summary

The following table outlines, for a range of authentication settings and SharePoint site artifacts, the resultant identity that is obtained from each of the variables that maintain a principal object.

Table 4

Author

Reza Alirezaei is a Microsoft Office SharePoint Server MVP, partner and founder of Dev Horizon. As a technical leader with eight years experience, he has helped many development teams architect and build large-scale, mission-critical applications. Reza has been focused on Collaboration, Knowledge Management, Business Process Management, and SharePoint technologies since its “Tahoe” days (2001). In addition to consulting, Reza is a SharePoint instructor and speaker. More information about Reza can be found at his blog.

References

http://www.microsoft.com/technet/security/guidance/serversecurity/serviceaccount/default.mspx
http://technet.microsoft.com/en-us/library/cc263468.aspx
http://technet2.microsoft.com/Office/en-us/library/f07768d4-ca37-447a-a056-1a67d93ef5401033.mspx?mfr=true
http://technet.microsoft.com/en-us/library/cc261736.aspx

Was this page helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft