Thursday, September 07, 2006

ASP.net session bump/hijack issue - F5 & IIS cluster

I haven't seen this issue personally, but it is confirmed from multiple sources this happens. In short: user rarely bumps into another user's session, in verbose: user logs into web app and uses it for a while and all of a sudden gets data that is not in his session.

Current prod infrastructure is IIS cluster behind F5. Session affinity is set to 20 mins and IIS session time-out is set 20 mins. Session location is in-proc.

I have seen request getting postedback to different webserver and hence viewstate validation fail with exception CryptographicException: Bad Data. Authentication of viewstate failed. ...
i don't know how F5 works but i guess F5 lost identifier of the user and hence thinks it as new user and routes request to different web server. This is a normal issue and we can either turnoff viewstate encryption or explicitly specify viewstate encryption key in all servers in cluster.

But i have to totally rule out F5 from the session bump/hijack issue, 'coz F5 has no idea what asp.net session is, that is something internal to aspnet_wp.exe (IIS5) or w3wp.exe (IIS6).

Under the hood, aspnet maintains the session id using ASP.NET_SessionId cookie and IIS can guarantee its uniqueness across its requests. Here is an MSFT KB about reusing session id for apps in same DNS domain and this article explains behind the scene how asp.net serves request.

I'm guessing this could be the cause,
userA hits webserver1 in cluster in the initial request and F5 should take us to the same server for subsequent requests.
If F5 sends the userA's request to webserver2 in middle of the session, and by happenstance (theoretically this may be possible) userA's ASP.NET_SessionId is already assigned by webserver2 to some other user (say userB), then aspnet engine will think userA as userB and assign userB's sessionstate to this request.

Pls leave a note if you think this may be possible or how you think this could be happening.

I expect IIS team to provide option to make the sessionid unique in a cluster environment. For now what we planned is to generate a unique sessionid and store it in cookie as well as session, check if they match for every request, if not kill the session, this is going to kill both user's session, but i don't know if there is any way out.

I didn't want to include tis logic in existing app 'coz 1 i'll have to include this logic in every app i write and 2 i'm too lazy. I wrote following HTTPModule and hooked it to web app, ideally something like this should be configured in machine.config so that we don't need to do this every web.config, netiher i don't have authority nor i want to pursue this.

Right now code is written to send a mail when something like this happens (just dropping a line to see if something takes this bait) and it will redirect to itself, so page is written poperly to handle empty session which i expect to be a norm.

using System;
using System.Web;
using System.Web.SessionState;

namespace KCC
{
public class SessionCheck : IHttpModule
{
public SessionCheck() {}
public void Dispose() {}
public void Init(HttpApplication httpApp)
{
httpApp.PreRequestHandlerExecute += new EventHandler(this.PreRequestHandlerExecute);
if (httpApp.Modules["Session"] != null)
{
SessionStateModule session = (SessionStateModule) httpApp.Modules["Session"];
session.Start += new EventHandler(this.OnSessionStart);
}
}

public void OnSessionStart(object sender, EventArgs e)
{
HttpCookie sessionChk = new HttpCookie("KCC.SessionCheck");
sessionChk["iSAS.SessionID"] = Guid.NewGuid().ToString();
sessionChk["iSAS.MacName"] = System.Environment.MachineName;
sessionChk["iSAS.SesStrtTmstmp"] = System.DateTime.Now.ToString();
HttpContext.Current.Response.AppendCookie(sessionChk);
HttpContext.Current.Session["iSAS.SessionID"] = sessionChk["iSAS.SessionID"];
}


private void PreRequestHandlerExecute(object sender, EventArgs e)
{
HttpApplication httpApp = (HttpApplication) sender;
HttpSessionState session = httpApp.Context.Session;
if (session != null) //skip pages which don't use session
{
if(session["iSAS.SessionID"] != null) //skip if session value is empty
{
if (!httpApp.Context.Session.IsNewSession) //skip if new session
{
string szCookieHeader = System.Web.HttpContext.Current.Request.Headers["Cookie"];
if ((null != szCookieHeader) && (szCookieHeader.IndexOf("ASP.NET_SessionId") >= 0)) //skip if new session 2
{
if (httpApp.Request.Cookies["KCC.SessionCheck"] != null) //skip ifCookie not found
{
if (httpApp.Request.Cookies["KCC.SessionCheck"]["iSAS.SessionID"] != null) //skip if cookie data not found
{
if (session["iSAS.SessionID"].ToString().Trim() != httpApp.Request.Cookies["KCC.SessionCheck"]["iSAS.SessionID"].ToString().Trim())
{
System.Text.StringBuilder info = new System.Text.StringBuilder();
if (httpApp.Request.Cookies["KCC.SessionCheck"]["iSAS.SesStrtTmstmp"] != null)
info.Append("Session start time: " + httpApp.Request.Cookies["KCC.SessionCheck"]["iSAS.SesStrtTmstmp"].ToString());
info.Append("<br/>");
if (httpApp.Request.Cookies["KCC.SessionCheck"]["iSAS.MacName"] != null)
info.Append("Session start web server name: " + httpApp.Request.Cookies["KCC.SessionCheck"]["iSAS.MacName"].ToString());
info.Append("<br/>");
info.Append("Current time " + System.DateTime.Now.ToString());
info.Append("<br/>");
info.Append("Current web server " + System.Environment.MachineName);
info.Append("<br/>");
info.Append("Logged on NTID" + httpApp.Context.Request.ServerVariables.Get("LOGON_USER").ToString());
info.Append("<br/>");
if (session["NTID"] != null) info.Append("Current NTID in session" + session["NTID"].ToString());
info.Append("<br/>");
info.Append("URL " + httpApp.Request.Url.ToString());
info.Append("<br/>");
System.Web.Mail.MailMessage msg = new System.Web.Mail.MailMessage();
msg.To = "me@mail.com";
msg.Subject = "-- SESSION HIJACK ISSUE --";
msg.From = "me@mail.com";
msg.Body = info.ToString();
msg.BodyFormat = System.Web.Mail.MailFormat.Html;
try
{
System.Web.Mail.SmtpMail.SmtpServer = "mailhost.com";
System.Web.Mail.SmtpMail.Send(msg);
}
catch //do nothing
{}
httpApp.Context.Response.Cookies["KCC.SessionCheck"].Expires = DateTime.Now.AddYears(-1);
session.Abandon();
httpApp.Context.Response.Write(httpApp.Context.Request.Url.AbsoluteUri.ToString());
httpApp.Context.Response.Redirect(httpApp.Context.Request.Url.AbsoluteUri.ToString(),true);
httpApp.CompleteRequest();
}
}
}
}
}
}
}
}
}
}

For further reading, Foiling Session Hijacking Attempts and HTTP application executes events sequence.

No comments: