Page Refresh Detection Using HttpModule






4.39/5 (9 votes)
How to detect and handle a page refresh using only an HttpModule
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;
//write the hidden field only if the request is made to the aspx-handler
if(context.Request.Path.ToLower().EndsWith(".aspx"))
{
//attach the stream that writes the hidden field
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)
{
//Read the buffer from the stream
string sBuffer = UTF8Encoding.UTF8.GetString(buffer, offset, count);
//when the end of the html-text is read
if (endOfFile.IsMatch(sBuffer))
{
//append the buffer
html.Append(sBuffer);
//and fire the matching for the start of the form-tag
//the form tag contains various additional attributes, therefore
//a non-greedy expression is used to find the whole opening tag.
MatchCollection aspxPageMatches =
Regex.Matches(html.ToString(),"<form[^>]*>",RegexOptions.IgnoreCase);
//When a form-tag could be found
if(aspxPageMatches.Count > 0)
{
StringBuilder newHtml = new StringBuilder();
int lastIndex = 0;
//usually only one form tag should be
//inside a html-text, but who knows ;)
for(int i = 0; i < aspxPageMatches.Count; i++)
{
//Get the text up to the form tag.
newHtml.Append(html.ToString().Substring(lastIndex,
aspxPageMatches[i].Index -lastIndex));
//get the opening form-tag
string key = aspxPageMatches[i].Value;
//generate the new hidden field
string enc = string.Format("\r\n<input id=\"{0}\" type" +
"=\"hidden\" name=\"{0}\" value=\"{1}\"/>",
HIDDEN_FIELD_ID, guid);
//write both the the html-text
newHtml.Append(key+enc);
lastIndex = aspxPageMatches[i].Index +
aspxPageMatches[i].Value.Length;
}
//append the rest of the html-text
newHtml.Append(html.ToString().Substring(lastIndex));
html = newHtml;
}
//write the whole text back to the stream
byte[] data = UTF8Encoding.UTF8.GetBytes(html.ToString());
responseStream.Write(data, 0, data.Length);
}
else
{
//when the end of the html-text is not found yet,
//write the buffer to the stringbuilder only
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 = "";
//Refreshing is only prohibited of the request is a post-request.
if(context.Request.HttpMethod.ToUpper().Equals("POST"))
{
//Get the guid from the http-post form
if(context.Request.Form!=null)
s = context.Request.Form[RefreshDetectionResponseFilter.HIDDEN_FIELD_ID];
//if the guid is already in the queue the post is a refresh
if(q.Contains(s) && s.Length>0)
{
//refresh -> Redirect to any other page
context.Response.Redirect("Logout.aspx");
context.Response.Flush();
context.Response.End();
}
//when the queue-size exceeded its limit (queueSize), guids will be
//removed from the queue until the queue size is lower than the limit.
while(q.Count>=queueSize)
q.Dequeue();
//since the post is not a refresh the guid is written to the queue
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