Skip to main content

MVC Bug: The virtual path '[path]' maps to another application, which is not allowed

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:

  1. Create a new Asp.Net MVC2 Web Application (from the template) called ‘Asp Net Bug 2008’
    • Don’t bother with the unit tests project
  2. 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!)
  3. Open the view Views/Account/Register.aspx
  4. Just after the line
    1. <% using (Html.BeginForm()) { %>
    Add the following:
    1. <%= Html.AntiForgeryToken() %>
  5. Compile and run
  6. Navigate to the [Log On] link at the top of the page
  7. Hit the ‘Register’ link

You will see this exception helper:

 mvcexception

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:





  1. <%= (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.CreateFormatterGenerator
Which 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):





  1. public static Func<IStateFormatter> CreateFormatterGenerator()
  2. {
  3.     // This code instantiates a page and tricks it into thinking
  4.     // that it's servicing a postback scenario with encrypted
  5.     // ViewState, which is required to make the StateFormatter
  6.     // properly decrypt data. Specifically, this code sets the
  7.     // internal Page.ContainsEncryptedViewState flag.
  8.     TextWriter writer = TextWriter.Null;
  9.     HttpResponse response = new HttpResponse(writer);
  10.     HttpRequest request = new HttpRequest("DummyFile.aspx",
  11.         HttpContext.Current.Request.Url.ToString(), "__EVENTTARGET=true&__VIEWSTATEENCRYPTED=true");
  12.     HttpContext context = new HttpContext(request, response);
  13.  
  14.     Page page = new Page()
  15.     {
  16.         EnableViewStateMac = true,
  17.         ViewStateEncryptionMode = ViewStateEncryptionMode.Always
  18.     };
  19.     page.ProcessRequest(context);
  20.  
  21.     return () => new TokenPersister(page).StateFormatter;
  22. }

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

Popular posts from this blog

Asp.Net 2 and 4 default application pool generates CS0016 IIS7.5

Before I start – if you’ve found a bunch of other articles about this around the net, tried the fixes that are mentioned and still not getting any joy – then read on – you might find this solves your problem. Earlier today I discovered that when I run any ASP.Net 2 or 4 application through IIS7.5 using the default application pools (which use ApplicationPoolIdentity) on Windows 2008 R2 x64 I get an error message similar to this: Server Error in '/MvcApplication31' Application. Compilation ErrorDescription: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.
Compiler Error Message: CS0016: Could not write to output file 'c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\mvcapplication31\222b4fe6\4e80a86\App_global.asax.clb4bsnc.dll' -- 'The directory name is invalid. '
Source Error:[No relevant source lines]
Sou…

Serializing to attributes in WCF with DataContractSerializer

It’s a common problem – you want to return an object from a WCF service as XML, but you either want, or need, to deliver some or all of the property values as XML Attributes instead of XML Elements; but you can’t because the DataContractSerializer doesn’t support attributes (you’re most likely to have seen this StackOverflow QA if you’ve done a web search).  Most likely you’ve then migrated all your WCF service code to using the XmlSerializer (with all the XmlElement/XmlAttribute/XmlType attributes et al) – and you’ve cursed loudly.Well, I’m here to rescue you, because it is possible – and the answer to the problem is actually inferred from the MSDN article entitled ‘Types supported by the Data Contract Serializer’.The example I’m going to give is purely for illustration purposes only.  I don’t have a lot of time, so work with me!Create a new Asp.Net WCF service application, you can use Cassini as your web server (probably easier – otherwise you might have to enable Asp.Net compatibil…

Adding ‘Deny’ functionality to AuthorizeAttribute in Asp.Net Web API

For the web service project I’m working on at the moment I need to be able to treat authorization differently based on the hostname of the URL that requests are made through.To state more clearly – these web services will have a ‘sandbox’ mode in addition to the real mode, and the mode a request will operate under is determined as part of the controller-selection phase early in the Web API request lifecycle.  So, say that my web services will be hosted on services.acme.com; the sandbox will simply be sandbox.services.acme.com.Please note – a discussion of how this is implemented is entirely outside the scope of this article; but I’ll just say that I’ve developed an in-house multi-tenancy layer for both MVC 4 and Web API that allows us to define ‘brands’ and, under those, you can then redefine content, controllers, and even the DI container that is used.These services are going to require caller-level authentication for most operations via SCRAM Authentication (RFC 5802), and as such m…