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?
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;
}
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.
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>