XAML and Activity Assembly Spoofing
Recently I was contacted by a customer who had read my posts on Versioning and was investigating some interesting behavior of activity designers (more on this in a future post). But this discussion got me to thinking. As I pointed out in my WF4 Activity Versioning Solution, with compiled workflows the XamlStaticHelper class loads assemblies into the app domain for you but what happens when you are using loose XAML with ActivityXamlServices.Load instead?
Download Windows Workflow Foundation (WF4) XAML Activity Spoofing
Watch AppFabric.tv - XAML and Activity Assembly Spoofing
The Security Model
When working with XAML, the security model is just like working with any other kind of scripting or dynamically compiled environment. When your XAML is loaded, and it requires an assembly, we simply ask the CLR to load it.
The security of the activity assemblies then comes down to one thing. You must protect the paths that the CLR will use when loading assemblies (For more on this see How the Runtime Locates Assemblies). If the attacker can replace an assembly that your code depends on, the game is pretty much over so you need to ensure that these paths are protected.
What happens if you don’t protect them? Read on…
The Scenario
In this scenario I have a workflow that receives a username and password which verifies that the user is valid and the password is also valid. The activities which do the authorization are in MySecurityActivity.dll and I have written a Workflow Console Application which invokes this workflow to authorize the user
Because MySecurityActivity.dll is security sensitive I’ve signed the assembly
I’ve also created the equivalent of the XAML workflow above in a code based workflow
private Activity CreateActivity()
{
var validUser = new Variable<bool>();
var validPassword = new Variable<bool>();
return new Sequence
{
Activities =
{
new IsValidUser
{
User = new InArgument<string>(ctx => this.User.Get(ctx)),
Result = new VariableReference<bool>(validUser)
},
new If
{
Condition = new InArgument<bool>(validUser),
Then = new IsValidPassword
{
Password = new InArgument<string>(ctx => this.Password.Get(ctx)),
Result = new VariableReference<bool>(validPassword)
}
},
new Assign
{
To = this.Authorized,
Value = new InArgument<bool>(ctx => validUser.Get(ctx) && validPassword.Get(ctx))
}
}
};
}
When I run my application I can log in. For testing purposes the only valid username is “Bob” and the only valid password is “secret”
The Spike
When you import a CLR namespace in XAML (with WF) only the short name of the assembly is used which leads to a number of questions.
- What happens if you replace MySecurityActivity.dll with an unsigned dll which allows anyone to log in?
- What about versioning? Does the same problem occur?
- Does using XAML or Code based workflow make any difference?
- What options do I have to deal with this situation?
What happens if you replace MySecurityActivity.dll with an unsigned dll which allows anyone to log in?
If I were an attacker and I could somehow replace MySecurityActivity.dll with a nefarious assembly I might be able to hack in to your system. Of course I would have to have a way to get my assembly on your system (remember you must protect your probing paths) but if I could then what…
To make this simple, I’ve created another project called MyEvilActivity which builds an unsigned assembly named MySecurityActivity.dll but it allows any user and any password. Then I created a batch file which will overwrite MySecurityActivity.dll.
Now when I run the application
Why does this happen?
When I added my activities to the workflow in the designer, the activity designer added an import to my XAML.
xmlns:m="clr-namespace:MySecurityActivity;assembly=MySecurityActivity"
You’ll notice that even though MySecurityActivity is signed, it used the Short Form of the assembly name. When the XAML is loaded if there is no assembly with the name MySecurityActivity already loaded into the AppDomain the XAML Schema Context will load any assembly that it finds matching that name regardless of strong name or version.
Does using XAML or Code based workflow make any difference?
If we try to replace MySecurityActivity with the evil one using a code based workflow this is what happens
Could not load file or assembly 'MySecurityActivity, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ebc9d3b9c8d1bf6b' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
Of course that makes sense because the CLR is enforcing the rules about strong names. For more on this see Strong-Name Signing for Managed Applications
Does using XAML or Code based workflow make any difference? Yes it does – code based workflows are not subject to this type of luring attack.
What about versioning? Does the same problem occur?
In my solution I also have MySecurityActivity.V2. With this version I added 2 to the end of the username “Bob2” and password “secret2”. What happens if I take version 1 of my host application and run it with version 2 of MySecurityActivity.dll?
With the XAML Workflow it happily loads and runs version 2 of MySecurityActivity.dll
With the code based workflow it fails with the same exception we saw earlier.
What options do I have to deal with this situation?
You have several options
- Use compiled workflows
- Author code based workflows
- Host loads the correct version of the assemblies prior to calling the workflow
- Edit the XAML to use the Fully Qualified assembly name
Use compiled workflows
When you use a compiled workflow, the XamlAppDef build task will generate code that will load the strongly named assemblies into the AppDomain.
Author code based workflows
Not many people do code based workflows. I can understand, after all part of the value of workflow is the designer. This is certainly an option but a rather drastic one.
Host loads the correct version of the assemblies prior to calling the workflow
With this option it becomes the responsibility of the hosting application to ensure that the correct version of the dependent assembly is loaded into the AppDomain prior to invoking the workflow. When the XAML schema context loads if it sees that there is already a MySecurity assembly loaded it will use that one.
I don’t like this option very much because it requires me to keep track of which versions of assemblies that the XAML is using. Where do I keep track of such things? In a file, a database? If I do track this I have to be concerned about my store getting out of sync with the workflow.
Edit the XAML to use the Fully Qualified assembly name
As you may know, WPF and WF share the same XAML stack so I read about WPF and Assembly Loading and discovered some interesting things. Until this morning I didn’t even realize that I could simply edit the XAML to use the Fully Qualified assembly name.
Note: if you are saving XAML from your own designer you can use XamlSchemaContextSettings.FullyQualifyAssemblyNamesInClrNamespaces to do this automatically
xmlns:m="clr-namespace:MySecurityActivity;assembly=MySecurityActivity, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ebc9d3b9c8d1bf6b"
By doing this I ensure that the XAML Schema context will use the Assembly FullName when loading the assembly and the behavior I get will be the same as using coded workflows.
In my view, this is the best choice for solving this problem.
Summary
The message here is be aware of the security model and careful to secure the probing paths. If you are loading XAML and invoking it you have both a potential security problem and a potential versioning problem. You should be aware of the issues raised here and take action to secure the environment before loading and running XAML just as you would with any dynamic compilation or scripting environment.
Happy Coding!
Ron Jacobs
https://blogs.msdn.com/rjacobs
Twitter: @ronljacobs https://twitter.com/ronljacobs
Comments
Anonymous
June 21, 2011
The problem we had in WF3/3.5 with assembly qualified names was when the assembly reference was a project in the solution. As the solution gets compiled, the version number changes because (at least with the default settings) the build and revision numbers are automatically assigned. This caused all sorts of grief because the xaml is now at best referencing an incorrect assembly and at worst is referencing an assembly that can no longer be found. This post got me thinking about partially qualified types and whether the CLR could resolve a type name that was partially qualified, but qualified enough to provide some sense of security in this context. I put together a test app to prove the case. The following piece of code is able to resolve the String type. Type typeName = Type.GetType("System.String, mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089"); This simply omits the version number but leaves in the culture and PublicKeyToken. This might be a better outcome than putting the assembly qualified name into the xaml.Anonymous
June 21, 2011
Yes this is what I call a "Version Independent" assembly load. This is actually what WF does today. First it tries to load the appropriate version, then if it can't load it, it strips away the version number and loads whatever version it can find. However this could lead to unexpected behavior like a V2 workflow loading a V1 assembly or vice-versa.