14

Normally i wouldn't put a title like this in the question, but i'm pretty sure it's a bug (or by design?)

I created a brand new ASP.NET MVC 3 Web Application.

Then i went to the /Home/About page.

The URL for this page is:

http://localhost:51419/Home/About

Then i changed the URL to this:

http://localhost:51419/(A(a))/Home/About

And the page worked? Looking at the route values, controller = Home, Action = About. It's ignored the first part?

And if i look at all the links in the source:

<link href="/(A(a))/Content/Site.css" rel="stylesheet" type="text/css" />
<script src="/(A(a))/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
<script src="/(A(a))/Scripts/modernizr-1.7.min.js" type="text/javascript"></script>

<li><a href="/(A(a))/">Home</a></li>
<li><a href="/(A(a))/Home/About">About</a></li>

See how it's maintained that first part? It's like the routing engine thinks it's part of the domain or something?

I've got a feeling it's a regex thing, because if i change the URL to:

http://localhost:51419/(a(a))/Home/About

(E.g changed the uppercase A to lowercase)

It 404's.

Can anyone shed some light on this? Is this a bug or by design?

RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • 2
    It's a rare thing that a post with "possible bug" in the title is up vote worthy, IMO. This one is. – Andrew Barber Feb 01 '12 at 04:41
  • 1
    @AndrewBarber - i know. :) It's pure fluke i came across this one. Google was indexing some weird URL's with guids in them, caused by a bug in our application. – RPM1984 Feb 01 '12 at 04:44

3 Answers3

5

This appears to be related to Cookieless Sessions in the ASP.NET pipeline. It strips that URL pattern inside CookielessHelper.cs (System.Web.Security) while processing the request:

    // This function is called for all requests -- it must be performant.
    //    In the common case (i.e. value not present in the URI, it must not
    //    look at the headers collection
    internal void RemoveCookielessValuesFromPath() 
    {
        // See if the path contains "/(XXXXX)/" 
        string   path      = _Context.Request.ClientFilePath.VirtualPathString; 
        // Optimize for the common case where there is no cookie
        if (path.IndexOf('(') == -1) { 
            return;
        }
        int      endPos    = path.LastIndexOf(")/", StringComparison.Ordinal);
        int      startPos  = (endPos > 2 ?  path.LastIndexOf("/(", endPos - 1, endPos, StringComparison.Ordinal) : -1); 

        if (startPos < 0) // pattern not found: common case, exit immediately 
            return; 

        if (_Headers == null) // Header should always be processed first 
            GetCookielessValuesFromHeader();

        // if the path contains a cookie, remove it
        if (IsValidHeader(path, startPos + 2, endPos)) 
        {
            // only set _Headers if not already set 
            if (_Headers == null) { 
                _Headers = path.Substring(startPos + 2, endPos - startPos - 2);
            } 
            // Rewrite the path
            path = path.Substring(0, startPos) + path.Substring(endPos+1);

            // remove cookie from ClientFilePath 
            _Context.Request.ClientFilePath = VirtualPath.CreateAbsolute(path);
            // get and append query string to path if it exists 
            string rawUrl = _Context.Request.RawUrl; 
            int qsIndex = rawUrl.IndexOf('?');
            if (qsIndex > -1) { 
                path = path + rawUrl.Substring(qsIndex);
            }
            // remove cookie from RawUrl
            _Context.Request.RawUrl = path; 

            if (!String.IsNullOrEmpty(_Headers)) { 
                _Context.Request.ValidateCookielessHeaderIfRequiredByConfig(_Headers); // ensure that the path doesn't contain invalid chars 
                _Context.Response.SetAppPathModifier("(" + _Headers + ")");

                // For Cassini and scenarios where aspnet_filter.dll is not used,
                // HttpRequest.FilePath also needs to have the cookie removed.
                string filePath = _Context.Request.FilePath;
                string newFilePath = _Context.Response.RemoveAppPathModifier(filePath); 
                if (!Object.ReferenceEquals(filePath, newFilePath)) {
                    _Context.RewritePath(VirtualPath.CreateAbsolute(newFilePath), 
                                         _Context.Request.PathInfoObject, 
                                         null /*newQueryString*/,
                                         false /*setClientFilePath*/); 
                }
            }
        }
    } 

Your pattern matches this:

    ///////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////// 
    // Make sure sub-string if of the pattern: A(XXXX)N(XXXXX)P(XXXXX) and so on. 
    static private bool IsValidHeader(string path, int startPos, int endPos)
    { 
        if (endPos - startPos < 3) // Minimum len is "X()"
            return false;

        while (startPos <= endPos - 3) { // Each iteration deals with one "A(XXXX)" pattern 

            if (path[startPos] < 'A' || path[startPos] > 'Z') // Make sure pattern starts with a capital letter 
                return false; 

            if (path[startPos + 1] != '(') // Make sure next char is '(' 
                return false;

            startPos += 2;
            bool found = false; 
            for (; startPos < endPos; startPos++) { // Find the ending ')'

                if (path[startPos] == ')') { // found it! 
                    startPos++; // Set position for the next pattern
                    found = true; 
                    break; // Break out of this for-loop.
                }

                if (path[startPos] == '/') { // Can't contain path separaters 
                    return false;
                } 
            } 
            if (!found)  {
                return false; // Ending ')' not found! 
            }
        }

        if (startPos < endPos) // All chars consumed? 
            return false;

        return true; 
    }
pjumble
  • 16,880
  • 6
  • 43
  • 51
  • Hmm, sooo...is this a bug then? Not sure if its an ASP.NET core thing, as that code makes sense. BUT, the ASP.NET routing engine (well, the MVC routing one at least), IMO shouldn't accept that as part of the route (e.g it shouldn't match). Should i raise this with MS? – RPM1984 Feb 01 '12 at 23:30
  • @RPM1984 No, it's clearly "by design" as how they track which user is logged in when cookies are not available. Just pretend you never noticed this and everything will be ok. – Robert Levy Feb 03 '12 at 16:30
  • @RobertLevy - i can't ignore it. This is actually happening to me with a live application. As i said in the comment, google are indexing some weird URL's which match this pattern. So because MVC is accepting them as valid routes, they are simply getting to my controllers then dying later on. When really they should be 404'ed from the get go. The URL's are also random, so i can't easily put a rewrite in place. Fair enough this is "by design" for ASP.NET, but i think this is a bug in MVC. It should NOT accept this as a route. – RPM1984 Feb 05 '12 at 23:22
  • It isn't random ... It starts with A( and ends with the following ) so you can safely code around it. If you do a rewrite though, your site will be broken for people with cookies disabled. – Robert Levy Feb 06 '12 at 13:16
  • @RobertLevy - i think your misunderstanding. ASP.NET actually strips out that part of the URL, so by the time it gets to my action, i can't actually see that in any part of the URL, and therefore cannot code around it. I'm thinking the best solution is to work out exactly the regex pattern ASP.NET is picking up, then put this in as a IIS rewrite. But as you say, it will be broken for people will cookies disabled. Might just have to leave it and hope Google stops hitting it. – RPM1984 Feb 07 '12 at 09:10
0

You might want to try adding an IgnoreRoute to your route mapping - but I admit I'm not sure what format to provide to match all your possible cookieless paths.

Basic
  • 26,321
  • 24
  • 115
  • 201
-1

I agree with @pjumble analysis, but not to his solution.
Disable cookieless authentication for either form authentication or anonymous authentication.

This will prevent users with cookies disabled to authenticate. But who cares anyway, everyone nowdays have cookies activated as no modern website will work without.

Add in web.config:

<anonymousIdentification enabled="false" />
<authentication mode="None" />

or

<anonymousIdentification enabled="true" cookieless="UseCookies" ... />
<authentication mode="Forms">
  <forms name="Auth" cookieless="UseCookies" ... />
</authentication>
Softlion
  • 12,281
  • 11
  • 58
  • 88
  • BTW, what was @pjumble's "solution", i dont see one, just a diagnosis. – RPM1984 Feb 02 '12 at 20:18
  • you would block some set of user from logging in just because of a silly meaningless quirk in .net's url parsing? – Robert Levy Feb 03 '12 at 16:29
  • Contest: give me a single web site which can work correctly at 100% with cookies disabled. – Softlion Feb 23 '12 at 19:04
  • @Softlion that'd be any site that doesn't use any session (pure information sites) They're getting rare I admit since everyone wants to personalize their web nowadays but they're still around :-) – IvanL Aug 09 '12 at 15:12
  • It's a question of economy. Is this feature more important than another which you won't have time to implement if you spent time on a workaround for this "problem" ? The sales department will prefer to have a new business feature to sell, more than requiring to be compatible with 10 users which have the nocookie browser extension. – Softlion Aug 21 '12 at 10:10