Click here to Skip to main content
Email Password   helpLost your password?

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

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
Generaldetect page refresh
Gangadhar Mahindrakar
20:14 18 Feb '09  
this is my first post,i have one idea of if User Click the Refresh button, we can detect it in if(page.ispostback){return;} this will detect the Refresh and keep original page...as it is.

this is my first post, if this approach is wrong then please give me idea of best approach.
GeneralRe: detect page refresh
Jens Meyer
21:28 18 Feb '09  
Basically you are right, but, the page.ispostback is also true if you hit a button on your page. A refresh is different to just hitting a button. You have the same data send to the server again. Test it: put a button on a page. Press the button and you will get the page.ispostback == true. The same happens if you press F5 after the button. The page is sent/posted again without pressing the button.

When in trouble,
when in doubt,
run in circles,
scream and shout

GeneralRe: detect page refresh [modified]
siphu
23:52 18 Feb '09  
Hi Jens,

You did a good job. I took a look at your article and there is one question that i could not answer myself. At your application_BeginRequest function, where you're based on the context to decide which method (POST or GET) the request contains. So, just based on the context (in this case is POST) we can know exactly the request is comming by pressing a F5 button? Can a button-based event contain POST method?

Sorry if any problems with my english.
Thanks

modified on Saturday, February 21, 2009 11:46 PM

GeneralRe: detect page refresh
Jens Meyer
2:21 19 Feb '09  
Thanks siphu,

in the context of a POST we know that the request is based on a button for example. But the context is also a POST if you pressed F5 after a POST (eg. a button press).
Since I wanted to redirect a refresh/F5 only I had to differ between a button press or a refresh/F5. To do so I injected a GUID in the previous page that was sent to the client. When I receive a request containing a GUID that I already received I know that the request is a refresh/F5. A refresh/F5 of a post sends the same data to the server again including the same GUID that i injected when I sent the page to the client.

In my case only the POST-Requests are relevant, coz they are the only ones that do critical operations (e.g. DB update/insert/delete or starting next level services).

I hope this makes more sense to you now.

Best regards

Jens

When in trouble,
when in doubt,
run in circles,
scream and shout

GeneralRe: detect page refresh
Phu Nguyen Si
19:05 21 Feb '09  
Thanks for your help, Jens. I figured it out.
GeneralWhat's the diffrent with SimoneB's solution
guaike
17:22 18 Feb '09  
Hi,What's the diffrent with SimoneB's solution? Smile
GeneralRe: What's the diffrent with SimoneB's solution
Jens Meyer
21:40 18 Feb '09  
Guaike,

the difference is that SimoneB's approach is to inject a field in the page that is about to be send to the aspx-handler. On the page itself she decides based on the field whether the page is a refreshed one or not. I used the same idea of detecting a refresh, but instead of leaving the handling in the aspx-handler (the page itself) I wanted to handle the refresh in the module by just redirecting the refresh to the logout-page.

The problem was that we have far too many pages to put a seperate handling on each page. So a general handling in a module was the only option.

Best regards

Jens

When in trouble,
when in doubt,
run in circles,
scream and shout

QuestionSource code?
Michael B. Hansen
0:40 17 Feb '09  
Hi,

First let me say - it's a nice idea.

But, where's the source code?

From your examples it is possible to derrive the class implementation - but requires a bit of work as a number of class variables etc. aren't obvious without the source code.


Best regards,

Michael
AnswerRe: Source code?
Jens Meyer
1:11 17 Feb '09  
Thanks Michael,

I was quite sure that I uploaded the sources as well. However, I updated the article (I screwed the web.config part up) together with the download and send it to them. Hopefully everything will be ok then.

When in trouble,
when in doubt,
run in circles,
scream and shout


Last Updated 17 Feb 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010