I’ve being doing a lot of work on Asp.Net MVC (now v2) over the past few months and, firstly, I have to say that it totally rocks.
I will not go into any real detail about MVC here – this post is about the method System.Web.Mvc.HtmlHelper.AntiForgerytoken and that it highlights a bug in an internal class TokenPersister that you’re potentially going to get if you ever use spaces in your virtual directory names. Ironically, the class in question is marked with a comment that says it’s difficult to unit test because of the way it fakes Asp.Net requests – ironic that it should be such a class in which we find a bug! A better argument for unit-testing you’re unlikely to find.
Detail – Reproduce that bug
Using MVC it’s very easy to reproduce. I should start by declaring my system configuration:
- Windows 2008 R2
- IIS 7.5 installed and configured
- Visual Studio 2008 with all service packs etc
- .Net 3.5 sp1 plus security patches
VS2010 and .Net 4 are also on this machine – and I would welcome anybody following the same steps to confirm that i:
- Create a new Asp.Net MVC2 Web Application (from the template) called ‘Asp Net Bug 2008’
- Don’t bother with the unit tests project
- Open the project web properties, and set the project to use the Local IIS Web Server, the location should automatically be set to http://localhost/Asp Net Bug 2008/ (you might need to add the trailing slash here) – the spaces here are important so leave them in.
- Create the Virtual Directory
- Don’t forget to save the project file afterwards (like I just did as I created this walkthrough!)
- Open the view Views/Account/Register.aspx
- Just after the line
- <% using (Html.BeginForm()) { %>
- <%= Html.AntiForgeryToken() %>
- Compile and run
- Navigate to the [Log On] link at the top of the page
- Hit the ‘Register’ link
You will see this exception helper:
And then on the Asp.Net error page, you’ll see this stack trace:
[ArgumentException: The virtual path '/Asp%20Net%20Bug%202008/Account/Register' maps to another application, which is not allowed.]
System.Web.CachedPathData.GetVirtualPathData(VirtualPath virtualPath, Boolean permitPathsOutsideApp) +11193138
System.Web.HttpContext.GetFilePathData() +61
System.Web.Configuration.HttpCapabilitiesBase.GetBrowserCapabilities(HttpRequest request) +124
System.Web.HttpRequest.get_Browser() +168
System.Web.UI.Page.SetIntrinsics(HttpContext context, Boolean allowAsync) +207
System.Web.UI.Page.ProcessRequest(HttpContext context) +232
System.Web.Mvc.TokenPersister.CreateFormatterGenerator() +459
System.Web.Mvc.FormatterGenerator..cctor() +10
[TypeInitializationException: The type initializer for 'FormatterGenerator' threw an exception.]
System.Web.Mvc.AntiForgeryDataSerializer.get_Formatter() +38
System.Web.Mvc.AntiForgeryDataSerializer.Serialize(AntiForgeryData token) +181
System.Web.Mvc.HtmlHelper.GetAntiForgeryTokenAndSetCookie(String salt, String domain, String path) +405
System.Web.Mvc.HtmlHelper.AntiForgeryToken(String salt, String domain, String path) +13
System.Web.Mvc.HtmlHelper.AntiForgeryToken() +17
ASP.views_account_register_aspx.__RenderregisterContent(HtmlTextWriter __w, Control parameterContainer) in c:\Users\andras.zoltan\documents\visual studio 2008\projects\aspnetbug2008\aspnetbug2008\Views\Account\Register.aspx:17
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +115
ASP.views_shared_site_master.__Render__control1(HtmlTextWriter __w, Control parameterContainer) in c:\Users\andras.zoltan\documents\visual studio 2008\projects\aspnetbug2008\aspnetbug2008\Views\Shared\Site.Master:26
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +115
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240
System.Web.UI.Page.Render(HtmlTextWriter writer) +38
System.Web.Mvc.ViewPage.Render(HtmlTextWriter writer) +89
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4240
A closer look at this, you’ll see that the method GetBrowserCapabilities, called from the accessor of the HttpRequest.Browser is the culprit. Opening Reflector, we see that this method is attempting to load the site configuration (web.config presumably) – which is where GetVirtualPathData comes in. When I was confronted with this issue, it was obvious that the check which determines if the current request is in the current application root was failing. So the first thing I thought was to change our app root to be something more friendly.
The Fix
Take the spaces out of the virtual directory name; perhaps replacing with hyphens, or just zeroing them down completely. It’ll now work perfectly.
The Cause
Well, of course I haven’t analysed down to the last IL opcode here, but the first thing I thought was whether or not this was an Asp.Net bug, since the last few items in the call stack are from there. So you can try another test, if you simply add a standard WebForm to the project (making sure it’s hosted in a vdir again that has spaces in it), and put in the following:
- <%= (Request.Browser!=null).ToString() %>
Compile and run that, and you’ll correctly get ‘True’ output in the page. So, when the current HttpRequest is produced from the normal Asp.Net pipeline, everything works correctly.
So, let’s take another look at that stack trace – we have the call to
System.Web.Mvc.TokenPersister.CreateFormatterGeneratorWhich the yields a call to
System.Web.UI.Page.ProcessRequest
But we’re already in that method further down, so why?
The reason can be found if you open CreateFormatterGenerator, either by getting the source code from CodePlex or by opening reflector. Let’s take a look at the method body (which I have ripped straight out of the published project from CodePlex):
- public static Func<IStateFormatter> CreateFormatterGenerator()
- {
- // This code instantiates a page and tricks it into thinking
- // that it's servicing a postback scenario with encrypted
- // ViewState, which is required to make the StateFormatter
- // properly decrypt data. Specifically, this code sets the
- // internal Page.ContainsEncryptedViewState flag.
- TextWriter writer = TextWriter.Null;
- HttpResponse response = new HttpResponse(writer);
- HttpRequest request = new HttpRequest("DummyFile.aspx",
- HttpContext.Current.Request.Url.ToString(), "__EVENTTARGET=true&__VIEWSTATEENCRYPTED=true");
- HttpContext context = new HttpContext(request, response);
- Page page = new Page()
- {
- EnableViewStateMac = true,
- ViewStateEncryptionMode = ViewStateEncryptionMode.Always
- };
- page.ProcessRequest(context);
- return () => new TokenPersister(page).StateFormatter;
- }
Well, the embedded comment there explains what’s going on – and it’s this fake request that’s causing the problem, clearly. Whatever it is that Asp.Net does to make sure we don’t get this error on a standard WebForm is not being done properly – although I’m not sure what that could be. My initial guess is that the %20 is being compared to the space in the path name, rather than being url-decoded first – and therefore a simple fix might be to change the call to HttpContext.Current.Request.Url.ToString() on the fourth code line above to pass in the url-decoded string. If I was feeling ambitious, I’d hack into the project and pursue a fix. However, my primary feeling is to report it to the Mvc team – which I have done here.
Comments
Post a Comment