Adam has asked for the wisdom of the Perl Monks concerning the following question:

I am working on a script that interacts with other programs. Below is a simple script that demonstrates a problem I have run into. This simple script increments a count in a file, and allows the page visitor to reset the count. My problem is, I don't want the user to 'accidentally' reset the count by 1) reseting the count using the button, and then 2) hitting refresh on their browser. and 3) breezing past the expired page warning because they didn't stop to read it.
Is there a way to easily keep a user from being able to refresh a page after doing something where refreshing would be bad (but not at other times)? I was thinking of redirecting back to the same page again, restoring the safety of hitting refresh but I would need to pass a hidden form element too, which I've yet to figure out how to do. Can anyone help me out here? Preferably with out using cookies? Thanks.
#!perl -w use strict; use CGI qw( :standard ); use CGI::Carp qw( fatalsToBrowser ); $|=1; my $count = 0; print header, start_html('Simple Counter'), "\n"; if( -e 'cnt.tmp' and not param('Start Over') ) { open COUNT, 'cnt.tmp' or die "Failed to open cnt.tmp, $!\n"; while(<COUNT>) { if( /^\s*(\d+)\s*$/ ) { $count = $1; last } } close COUNT or die "Failed to close cnt.tmp, $!\n"; } print start_form(), "Count equals $count", br(), "\n", submit( -name=>"Again" ), submit( -name=>"Start Over", -value=>"Start Over" ), end_form(), "\n"; open COUNT, '>cnt.tmp' or die "Failed to open >cnt.tmp, $!\n"; print COUNT ++$count; close COUNT or die "Failed to close >cnt.tmp, $!\n"; print end_html();

Replies are listed 'Best First'.
Re: CGI Refresh question
by chromatic (Archbishop) on Jul 25, 2000 at 04:08 UTC
    Hidden elements are pretty easy, and you're using CGI.pm, so getting at them is really easy. Of course, if you know how to make them, spoofing them is also easy, so be aware that this isn't sufficient security unless your users are completely ignorant of View Source.
    # the HTML for making a hidden field # <input type="hidden" name="submitted" value="1"> if (! defined param('submitted') ) { # display a button } else { # don't display the button }
    To get around the refreshing bit is more difficult. Perhaps the default behavior would be *not* to increment the counter.

    (Counters are inaccurate anyway, due to proxies, refreshes, and other things. I think you just picked it as an easy example, though.)

      Thank you chromatic, but as I said in my post, my sample code is a simplistic demo of a much more complicated script. Counting had nothing to do with it, it was just a way to make it simple. As for the insecurity of the hidden tag, I'm not using it for security, I just use it track which app this button talks to. In other words, imagine I was writting the script for n counters, and you could reset the one of your choice. I am using hidden data for that.
      for( 1..5 ) { print start_form(), hidden( -name=>"cntrnum", -value=>$_ ), submit( -name=>"action", -value=>"Reset Counter" ), end_form(), "\n\n"; }
      The real heart of the question is how to keep the user from accidentally reseting the counter by refreshing. (And again, counters are just an easy substitute for a bigger task).

      And yes, I suspect that the users are ignorant of "View Source" which is why I'm so worried that they will refresh themselves into a bad spot. Y'see?

      Thanks again!

Re: CGI Refresh question
by turnstep (Parson) on Jul 25, 2000 at 05:17 UTC

    One way to do it that seems to work on my limited testing is something like this:

    #!perl ## Do the work here, process the form, etc. then: print "Location: http://www.foobar.com/StaticWithFormStuff.html\n\n";
    Refreshing brings up the "new" page, not the script that directed them there.

    A second way is to set the page as "expired" - some browsers will then complain if you try and refresh it.

    A third way is to keep track of the page via a unique URL or a hidden tag and store this info on the server somewhere. If the unique tag comes up twice, you've got a "repeat offender." Lots of overhead for the server, but the most reliable solution, as you are relying on your server to do the work, and not the unknown behavior of the user's browser.

Re: CGI Refresh question
by tye (Sage) on Jul 25, 2000 at 05:46 UTC

    Make the hidden parameter the timestamp when the web page was generated. Have an empty, web-writable file for each button on the page. When you get a button push, compare the hidden field to the timestamp on that button's file. If the time stamp is between that timestamp and "now", then honor the request and "touch" that button's file (update the file's timestamp). If the timestamp is too old, don't honor the request and respond saying that that resource was recently change so their change was ignored. If the timestamp is in the future, complain and "do something appropriate".

    Note that this also has the benefit of preventing me from updating an item that my web page says should be updated but that Adam update while I was looking at my page. This is probably a good thing.

      Hang on, thereīs a trap you might fall into. Donīt forget, that multiple users might call the same cgi at the same time! That will mess up your time stamp files!

      To make things worse, since HTTP is stateless, you have to write a complete session management yourself if you need one (can be done with the hidden fields mentioned, among others). The initial description suggests (at least to me) that you need such a "session concept".

      Andreas

        Yes, there is a race condition where multiple clients can reset the same counter in rapid succession and both succeed. However, this race doesn't affect the requested purpose, that is, people will not be allowed to reset a count by doing a refresh/reload of a page. That is mostly why I didn't mention the race condition originally.

        A more important flaw (to my eye), is that the solution doesn't distinguish between refusing my reset request because someone else beat me to it or because I resent a stale POST.

        The "right" way to remove the race condition is to lock the timestamp file across the test and set. However, that wouldn't fix the bigger flaw. My subconscious has been wondering if there was a (fairly) simple trick for solving both problems (like append the process ID/thread ID/client address&port to the time stamp file, check if you were first, etc.). This type of problem often looks simple but usually isn't. Anyway, I never came up with one.

        So lock the file, check the file's last modified time, and then either rewrite it with a unique ID that you also include hidden in each form or read the ID to determine which error to report, then unlock the file.

(CMonster: use the redirect) RE: CGI Refresh question
by CMonster (Scribe) on Jul 25, 2000 at 18:40 UTC

    I have to agree with turnstep's first suggestion, since that's the way I code CGI in general. When doing any real work behind the scenes, use a script that redirects the user to a static version of the output to avoid the refresh problem.

    In your case, if you're interested in having just one script, make it callable in the default way (which just displays the current value of the counter), the incrementor way (which increments the counter then redirects to the default way), and the reset way (which resets and redirects).

    Here's a simple example:

    #!perl -w use strict; use CGI qw( :standard ); use CGI::Carp qw( fatalsToBrowser ); $|=1; my $count = 0; if( -e 'cnt.tmp') and not param('Start Over') ) { open COUNT, 'cnt.tmp' or die "Failed to open cnt.tmp, $!\n"; while(<COUNT>) { if( /^\s*(\d+)\s*$/ ) { $count = $1; last } } close COUNT or die "Failed to close cnt.tmp, $!\n"; } if (param('Again') or param('Start Over') { open COUNT, '>cnt.tmp' or die "Failed to open >cnt.tmp, $!\n"; print COUNT ++$count; close COUNT or die "Failed to close >cnt.tmp, $!\n"; print "Location: this.cgi\n\n"; } else { print header, start_html('Simple Counter'), "\n"; print start_form(), "Count equals $count", br(), "\n", submit( -name=>"Again" -value=>"Again"), submit( -name=>"Start Over", -value=>"Start Over" ), end_form(), "\n"; print end_html(); }
RE: CGI Refresh question
by Adam (Vicar) on Jul 25, 2000 at 22:41 UTC
    I'm handling this with a redirect, the only thing I don't like is that it puts the paramater on the url line of the browser. Not too big a deal, just not asthetically (sp?)pleasing.
    if( param('Start Over') ) { open COUNT, '>cnt.tmp' or die "Failed to open >cnt.tmp, $!\n"; print COUNT '0'; close COUNT or die "Failed to close >cnt.tmp, $!\n"; print redirect( -location=>url() . "?HiddenTag=$HiddenValue" ); }
    And that works fine. Thanks for the help all! (And if anyone knows how to get that param through the redirect without putting it on the url line, let me know!)
      aesthetically or esthetically.

      hey, you asked! :)

      -- ar0n

      I've never found a way, but you can make your script name very similar to the static page name, and use POST instead of GET to minimize differences in appearance. Having your cgi scripts run from sonewhere other than "cgi-bin" helps as well.