Introduction
This article describes the detection and handling of page refreshes using only an HttpModule
.
Background
Critical operations done by web pages (e.g. writing stuff to databases, creating users, or deletion of stuff) should be secured from various actions. These actions are, for example, unauthorized access or multiple execution by simply refreshing the page. When I got into the situation (and also the project) that the refreshing issue came up, the project was already way too big (200+ pages, different kinds of users, etc.) to do anything on a per-page basis.
My colleagues already tried to convince our customer to not use F5 or the Refresh button of their browsers. They agreed, but users make mistakes as everyone knows, so from time to time, they do a page refresh. Since our logging is quite extensive, we were able to detect the error that occurred due to the refresh, but it is not satisfying to discuss with the customer over and over again about this issue.
However, I decided to write an HttpModule
that covers this topic somehow. So, I read articles about page refreshing detections on MSDN, and this one: Using an HttpModule to detect page refresh, for instance. But they all required additional work on the web pages. In my case, it was totally OK to redirect refreshed pages, so I decided to take the basic idea on refreshing detection and put it in my own module.
About the Code
The code is written for .NET 1.1, but should work in later versions of .NET also. However, I have not tested it on later versions.
Basic Idea
The basic idea on the solution is to write a hidden field in the page containing a unique key right before the page is transmitted to the client.
Using the Code
Compile the code and add the resulting library to the web.config file, as follows:
<httpModules>
<add name="RefreshDetectionModule" type="HttpModules.RefreshDetectionModule"/>
</httpModules>
Page-refresh Detection, Step One
To differ an HTTP-POST from another one, I decided to stick with the idea of injecting a (more or less) unique ID in every page that gets sent to the client. To achieve this, I wrote my own class that inherits from the Stream
class and hooked it to the Response.Filter
.
private void application_PreRequestHandlerExecute(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
if(context.Request.Path.ToLower().EndsWith(".aspx"))
{
application.Response.Filter =
new RefreshDetectionResponseFilter(application.Response.Filter,
Guid.NewGuid());
}
}
The stream-class (RefreshDetectionResponseFilter
) basically just needs to override the Write
-method. I write the whole stream to a StringBuilder
and search in the resulting HTML-text for the form
tag. The idea is to place the hidden field right behind the form
tag. Once the whole stream is read and stored in the StringBuilder
, I start searching for the form
-tag by using a Regular Expression. Having the form
-tag found, I append the hidden field to it.
public override void Write(byte[] buffer, int offset, int count)
{
string sBuffer = UTF8Encoding.UTF8.GetString(buffer, offset, count);
if (endOfFile.IsMatch(sBuffer))
{
html.Append(sBuffer);
MatchCollection aspxPageMatches =
Regex.Matches(html.ToString(),"<form[^>]*>",RegexOptions.IgnoreCase);
if(aspxPageMatches.Count > 0)
{
StringBuilder newHtml = new StringBuilder();
int lastIndex = 0;
for(int i = 0; i < aspxPageMatches.Count; i++)
{
newHtml.Append(html.ToString().Substring(lastIndex,
aspxPageMatches[i].Index -lastIndex));
string key = aspxPageMatches[i].Value;
string enc = string.Format("\r\n<input id=\"{0}\" type" +
"=\"hidden\" name=\"{0}\" value=\"{1}\"/>",
HIDDEN_FIELD_ID, guid);
newHtml.Append(key+enc);
lastIndex = aspxPageMatches[i].Index +
aspxPageMatches[i].Value.Length;
}
newHtml.Append(html.ToString().Substring(lastIndex));
html = newHtml;
}
byte[] data = UTF8Encoding.UTF8.GetBytes(html.ToString());
responseStream.Write(data, 0, data.Length);
}
else
{
html.Append(sBuffer);
}
}
When the hidden field is appended, I write everything back to the stream.
Page-refresh Detection, Step Two
Now that all the pages contain the hidden field, I just need to look out for the value of the hidden field, once the page is posted back. To do so, I just hook up to the BeginRequest
-event of the HttpModule and look in the posted form for the hidden field. If the value of the hidden field has been posted before, I know that this post is just the refreshing of a previously posted page. In that case, I just do a redirect to the logout-page.
If the value has not been posted before, I write the value to a list (I use a queue for easily dequeueing items when the list exceeds its maximum size).
private void application_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
string s = "";
if(context.Request.HttpMethod.ToUpper().Equals("POST"))
{
if(context.Request.Form!=null)
s = context.Request.Form[RefreshDetectionResponseFilter.HIDDEN_FIELD_ID];
if(q.Contains(s) && s.Length>0)
{
context.Response.Redirect("Logout.aspx");
context.Response.Flush();
context.Response.End();
}
while(q.Count>=queueSize)
q.Dequeue();
q.Enqueue(s);
}
}
Conclusion and Personal Notes
I wrote this module just to provide an easy way of detecting page-refreshes. In my case, it works good. I'm quite sure there is much to improve, so if anyone has ideas for improvements, I'll be happy to hear them. This is my first article, so please excuse me if I have made any mistakes.
History
- 16th February, 2009: Initial post
- 17th February, 2009: Updated article and added source code