Sunday, November 16, 2008

Creating a Custom CAS Policy File For SharePoint

Some Reasons

Maybe you're tired of putting your SharePoint web application in full trust and running web applications with huge security surface areas. Or maybe you've become tired of GAC'ing assemblies just so that they can run in full trust. Heck, maybe you've even read this article and decided to finally start handling Code Access Security in a more elegant way.

All of the above are good reasons to make a custom Code Access Security policy file, and in the following section we're going to do just that. If you're curious as to why you might be doing this in the first place, it might be a good idea to peruse common code access security issues described here.

Creating A Custom Policy File

The intent of the custom policy we're about to make is to allow your code and only your code to run in full trust regardless of where it is (doesn't have to be in the GAC). All other code in the application will be running as WSS_Minimal which means it will have a reduced set of privileges (see table). For example, codes running in WSS_Minimal can't access the SharePoint object model. Your code however will be running in full trust and will be able to do whatever it wants.

This is often desirable since we have no idea what web parts information workers or administrators will lob into our SharePoint application. It's a little presumptuous to assume that they're all safe and won't do anything malicious. Granting them full trust allows them to do things like write to sensitive areas of the disk, access the registry, etc... Users with high privileges (ie. administrators) get duped in to running malicious code all the time, that's one of the reasons Code Access Security exists in the first place. If you're curious about what the difference between code running in full trust and code running in WSS_Minimal is, refer to the table in the link above. Onward.

  1. First make a copy of C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\CONFIG\WSS_Minimal.config and put it in your application folder, call it WSS_Custom.config. If you want to increase the privileges of other code you can start with WSS_Medium, this will allow other code to access the SharePoint API without having security exceptions thrown.
  2. Strong name your assembly. We're not going to put in the GAC, but it still needs to be strong named so that it can be uniquely identified by the WSS_Custom.config policy file.
  3. Open up the Visual Studio Command Prompt (or go find sn.exe) and extract the public key from your .snk file into another file. (It's worth noting that the Public Key is not the same as the Public Key Token that you might get from your assembly using Reflector).
    sn -p YourStongNameFile.snk PublicKeyOnly.snk
    You should see the output "Public key written to PublicKeyOnly.snk".
  4. Now we print out that public key with the following command:
    sn -tp PublicKeyOnly.snk
  5. Copy that insanely long stream of numbers (your assembly's public key). Add the following line to your WSS_Custom.config just below the <CodeGroup class="FirstMatchCodeGroup"...>'s <IMembershipCondition> element.
    <CodeGroup class="FirstMatchCodeGroup" version="1" PermissionSetName="Nothing">
    <IMembershipCondition class="AllMembershipCondition" version="1" />
    <!--[Your Entry Here]-->
    <CodeGroup class="UnionCodeGroup" version="1" PermissionSetName="FullTrust">
    <IMembershipCondition class="StrongNameMembershipCondition" version="1" PublicKeyBlob="[Your Public Key]"></IMembershipCondition>
    </CodeGroup>
    <!--[End Addition]-->
    ...
    </CodeGroup>
  6. Now ensure that you're web.config uses your custom policy file. Add the following line right under <SecurityPolicy>:
    <trustLevel name="WSS_Custom" policyFile="[path to wss_custom.config]" />
  7. Finally change your web applications trust level to use your custom policy file. Ensure that the <trust/> element looks like below:
    <trust level="WSS_Custom" originUrl="" />
  8. That's it, you're done! You may need to restart IIS if you get an "Assembly <assemblyName> security permission grant set is incompatible".

I've posted a sample WSS_Custom.config if the above was confusing.

If you were getting security exceptions before they should be gone now, at least all those coming from your assembly. Other code will continue to run in WSS_Minimal (or WSS_Medium if you used that .config as a template) and may throw security exceptions should they try to access APIs that they don't have the privileges to do so.

This is a lot more preferable than two popular alternatives:

  1. Having the entire web application run in full trust (which gives all assemblies full trust).
  2. Putting your assemblies in the GAC just so that they can run in full trust.

Hope that helps.

Best,
Tyler

18 comments:

Anonymous said...

great post! Thanks

Malcolm said...

Hi Tyler

Great article, thanks!

I seem to keep running into the issue of safe controls when I sign my assembly. I had an unsigned web part, deploying to the bin directory and it worked fine. I had need to use LINQ in the web part and trust was set to full. I followed the steps in your article to get away from having trust set to full - it all seemed to make sense and I got everything done. Now, when I try and place the web part on the page I get a messagebox saying that assemblies that implement ASP.Net web parts and are installed in a partially trusted location such as the bin directory must be compiled with AllowPartiallyTrustedCallersAttribute set for import to succeed.

Does this make sense? I thought this assembly was now trusted as a result of the changes made to the CAS config. Any thoughts?

Thanks

Malcolm

Tyler Holmes said...

You're right Malcolm, your assemblies are indeed running in full trust. The problem is your web application isn't (it's probably still in WSS_Minimal/WSS_Medium).

When code that isn't running in full trust makes calls to code that IS running in full trust there's a risk that the calling code is luring the called assembly in to doing something malicious on it's behalf.

If you're sure this isn't a risk, then you can put the AllowPartiallyTrustedCallers attribute on your assembly.

There's more info on APTCA here (http://blogs.msdn.com/shawnfa/archive/2005/02/04/367390.aspx)

Best,
Tyler

Seung-Rak said...

Hi Tyler,

After I followed the same steps described, that is, as soon as I replaced "Full" with "WSS_Custom", I complete lost access to the portal with this error below:

Server Error in '/' Application.
--------------------------------------------------------------------------------

Security Exception
Description: The application attempted to perform an operation not allowed by the security policy. To grant this application the required permission please contact your system administrator or change the application's trust level in the configuration file.

Exception Details: System.Security.SecurityException: Request failed.

Source Error:

The source code that generated this unhandled exception can only be shown when compiled in debug mode. To enable this, please follow one of the below steps, then request the URL:

1. Add a "Debug=true" directive at the top of the file that generated the error.

or:

2) Add the following section to the configuration file of your application:

Note that this second technique will cause all files within a given application to be compiled in debug mode. The first technique will cause only that particular file to be compiled in debug mode.

Important: Running applications in debug mode does incur a memory/performance overhead. You should make sure that an application has debugging disabled before deploying into production scenario.

(i excluded stack trace because it exceeded the number of characters allowed)

Is there any other step or configuration that must have been arranged? Any opinion would be appreciated.

Thanks.

Tyler Holmes said...

Hey Sung-Rak,

It's very likely that your custom CAS policy is working correctly...it's just that there's other code that is throwing a CAS exception. This information would be in the stack trace.

I would suggest running your code in a blank site collection if you wanted to test your own CAS policy.
OR
Stare intently at the stack trace and try to discern what code is throwing the exception. If you can find out which assembly is responsible for the CAS exception then you can have it run in full trust by modifying your custom CAS policy.

You can extract public keys from signed assemblies for which you don't have the .snk by running sn -Tp [assemblyname].

Hope that helps.

My Best,
Tyler

Seung-Rak said...

Thanks for your prompt response, Tyler. I will be looking at the code, but in the meantime, please see if it is really the code that is generating this problem.

[SecurityException: Request failed.]
System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandle& ctor, Boolean& bNeedSecurityCheck) +0
System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean fillCache) +86
System.RuntimeType.CreateInstanceImpl(Boolean publicOnly, Boolean skipVisibilityChecks, Boolean fillCache) +230
System.Activator.CreateInstance(Type type, Boolean nonPublic) +67
System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes) +1051
System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes) +111
System.Web.Configuration.PagesSection.CreateControlTypeFilter() +8704955
System.Web.UI.PageParserFilter.Create(PagesSection pagesConfig, VirtualPath virtualPath, TemplateParser parser) +13
System.Web.UI.TemplateParser.ProcessConfigSettings() +224
System.Web.UI.TemplateControlParser.ProcessConfigSettings() +13
System.Web.UI.UserControlParser.ProcessConfigSettings() +12
System.Web.UI.TemplateParser.PrepareParse() +141
System.Web.UI.TemplateParser.Parse() +167
System.Web.UI.TemplateParser.Parse(ICollection referencedAssemblies, VirtualPath virtualPath) +34
System.Web.Compilation.BaseTemplateBuildProvider.get_CodeCompilerType() +85
System.Web.Compilation.BuildProvider.GetCompilerTypeFromBuildProvider(BuildProvider buildProvider) +62
System.Web.Compilation.BuildProvidersCompiler.ProcessBuildProviders() +199
System.Web.Compilation.BuildProvidersCompiler.PerformBuild() +42
System.Web.Compilation.BuildManager.CompileWebFile(VirtualPath virtualPath) +8732923
System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile) +261
System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile) +101
System.Web.Compilation.BuildManager.GetVPathBuildResult(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile) +83
System.Web.UI.TemplateControl.LoadControl(VirtualPath virtualPath) +48
System.Web.UI.TemplateControl.LoadControl(String virtualPath) +26
SharePointHQ.WebPart.AudienceMaster.AudienceMasterWebPart.CreateChildControls() +80
System.Web.UI.Control.EnsureChildControls() +87
System.Web.UI.Control.PreRenderRecursiveInternal() +44
System.Web.UI.WebControls.WebParts.WebPart.PreRenderRecursiveInternal() +42
System.Web.UI.Control.PreRenderRecursiveInternal() +171
System.Web.UI.Control.PreRenderRecursiveInternal() +171
System.Web.UI.Control.PreRenderRecursiveInternal() +171
System.Web.UI.Control.PreRenderRecursiveInternal() +171
System.Web.UI.Control.PreRenderRecursiveInternal() +171
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +6785
System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +242
System.Web.UI.Page.ProcessRequest() +80
System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context) +21
System.Web.UI.Page.ProcessRequest(HttpContext context) +49
ASP.DEFAULT_ASPX__82859748.ProcessRequest(HttpContext context) +4
System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +181
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +75

Tyler Holmes said...

Hey Sung-Rak,
There's not a tonne of information in that stack trace, do you what it's trying to construct?

I'm not even positive this is a code access security exception, usually they look something like:

Request for the permission of type
Microsoft.SharePoint.Security.SharePointPermission,
Microsoft.SharePoint.Security, Version=11.0.0.0, Culture=neutral,
PublicKeyToken=71e9bce111e9429c failed.

I would consider slowly deconstructing your page to try and isolate the code/component that is generating this exception.

Hope that helps.

Best,
Tyler

Chris Speck said...

Seung-Rak

I was getting a very similar error (not sure if it was the same, as it states Request Failed on that same method, but does not specify Security Exception (could not get this info anywhere).

The line of code which was causing this issue for me was

Page.LoadControl("~/controltemplates/mystuff/mycontrol.ascx");

I modified this to

Page.LoadControl(typeof(MyControl), null);

or

MyControl myctl = new MyControl();
Page.Controls.Add(myctl);

And was able to get passed that issue.

Clark said...

Thanks for the reply Chris.

but I am trying to load a specific user control. If you use the code snippet,

Page.LoadControl(typeof(MyControl), null);

or

MyControl myctl = new MyControl();
Page.Controls.Add(myctl);

How am I going to load the specific control that I want to load?

Chris Speck said...

Yes it turns out my solution above is not entirely correct. Apparently there is an issue with using LoadControl -- it requires that the PermissionSet tag contains Unrestricted="true", which _cannot_ be set in the solution manifest (stsadm throws error about invalid Unrestricted attribute). Read through some of the comments here:

http://www.dotnetmonster.com/Uwe/Forum.aspx/dotnet-security/2519/SecurityException-Request-failed-in-LoadControl

One solution is to manually modify the customtrust.config file which is used, and add the Unrestricted="true", however whenever the solution is updated, this is removed again. I have not yet found another solution.

Tyler Holmes said...

I believe this is happening because the assembly is not running in full trust and does not have the required permissions to reflect on some other control/assembly.

To remedy this you should consider either:
1) Creating a custom CAS policy for your application granting your assembly full trust.
2) GACing the assembly that does the reflecting.
3) Raise the trust level of the application (I'd recommend against this).

A table listed in the following post shows that neither WSS_Minimal of WSS_Medium grant the necessary trust level to reflect on other assemblies.

http://blog.tylerholmes.com/2008/10/don-set-your-sharepoint-app-to-full.html

Let me know if I'm missing something.

Best,
Tyler

Chris Speck said...

Tyler,

Regarding your first option listed in your comment above, Creating a custom CAS Policy:

This is what we've been trying to accomplish, but as it turns out, is not completely possible to define in the solution manifest. The LoadControl() method requires that the PermissionSet node in CAS has Unrestricted="true", which is not valid when placed inside the manifest.xml file of the solution. You have to manually add this attribute into the .config file on the server in order to accomplish this. It's rather bothersome - I hope this can be fixed with the next update of .Net Framework.

Christian said...

OK! So I followed your example but inside the root web.config as opposed to just Sharepoint but now I get:

Parser Error Message: An error occurred loading a configuration file: Request for the permission of type 'System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed. (machine.config)

Source Error:


[No relevant source lines]


Source File: machine.config Line: 161


Line 161 in my machine.config is my custom MembershipProvider:

add name="NetPerfectMembershipProvider"
type="NetPerfect.MembershipProvider,NetPerfect,version=3.5.0.0,Culture=neutral,PublicKeyToken=9a65a3585a4c74a6"......

If I dont add it as an IMembershipCondition, then it works fine, but I need to add another functional part that requires reading the siteid from the applicationhosts.config file that will require fulltrust!

Any ideas why would be much appreciated :s

Jason Apergis said...

Try moving the Page.LoadControl method to OnInit method instead of in the CreateControls. I was getting the exact same error that control could not be found.

I saw the article about changing the CAS permission but there are long term issues with that as Chris correctly pointed out.

Anonymous said...

Add even a UrlMemberShipCondition which should solve your issue.

Howard said...

Other people have left various solutions to this problem. I tried them, but only came up with one that worked.

I've Blogged the solution here: http://spfarm.blogspot.com/2011/02/loadcontrol-request-failed.html

Cheers
Howard

Hitesh said...

After apply my custom Code policy file my application comes with error.

Resource not found 404
/default.aspx

but with GAC deployment its working fine.

ArunKumar Dushakanti said...

Hi Tyler,

I have deployed my aspx pages to _layouts of SharePoint web application and the DLL's to the GAC. But unfortunately it still gives me a permission issue for one of my Oracle Client dll's. We tested it by raising the Trust level to "Full'and our application works fine after the change. We don't want to keep the Trust level as Full as it increases the security risk. Can you please let me know if we still need to add the CAS policy for the Third Party Oracle Client even if its deployed to GAC? I really appreciate your help on this as i am running out of my options.