NAME

CGI::SecureState -- Transparent, secure statefulness for CGI programs

SYNOPSIS

    use CGI::SecureState;

    my @memory = qw(param1 param2 other_params_to_remember);
    my $cgi = new CGI::SecureState(-stateDir => "states",
                                   -mindSet => 'forgetful',
                                   -memory => \@memory);

    print $cgi->header(), $cgi->start_html;
    my $url = $cgi->state_url();
    my $param = $cgi->state_param();
    print "<a href=\"$url\">I am a stateful CGI session.</a>";
    print "<a href=\"other_url.pl?$param\">I am a different ",
          "script that also has access to this session.</a>";

Very Important Note for Users of CGI::SecureState 0.2x

For those still using the 0.2x series, CGI::SecureState changed enormously between 0.26 and 0.30. Specifically, the addition of mindsets is so important that if you run your old scripts unchanged under CGI::SecureState 0.3x, you will receive nasty warnings (likely both in output web pages and your log files) that will tell you not to do so. Please do yourself a favor by re-reading this documentation, as this mysterious mindset business (as well as all the scrumptious new features) will be made clear.

Of course, any and all comments on the changes are welcome. If you are interested, send mail to behroozi@cpan.org with the subject "CGI::SecureState Comment".

DESCRIPTION

A Better Solution to the stateless problem.

HTTP is by nature a stateless protocol; as soon as the requested object is delivered, HTTP severs the object's connection to the client. HTTP retains no memory of the request details and does not relate subsequent requests with what it has already served.

There are a few methods available to deal with this problem, including forms and cookies, but most have problems themselves, including security issues (cookie stealing), browser support (cookie blocking), and painful implementations (forms).

CGI::SecureState solves this problem by storing session data in an encrypted state file on the server. CGI::SecureState is similar in purpose to CGI::Persistent (and retains much of the same user interface) but has a completely different implementation. For those of you who have worked with CGI::Persistent before, you will be pleased to learn that CGI::SecureState was designed to work with Perl's taint mode and has worked flawlessly with mod_perl and Apache::Registry for over two years. CGI::SecureState was also designed from the ground up for security, a fact which may rear its ugly head if anybody tries to do something tricksy.

MINDSETS

If you were curious about the mindset business mentioned earlier, this section is for you. In the past, CGI::SecureState had only one behavior (which I like to call a mindset), which was to store all the CGI parameters that the client sent to it. Besides bloating session files, this mindset encouraged all sorts of insidious bugs where parameters saved by one script would lurk in the state file and cause problems for scripts down the line.

If you could tell CGI::SecureState exactly which parameters to save, then life would get much better. This is exactly what the shiny new "forgetful" mindset does, as it will only store parameters that are in its "memory". The old behavior remains, slightly modified, in the form of the "unforgetful" mindset, which will cause CGI::SecureState to save (and recall) all parameters passed to the script excepting those that are in its "memory".

You may wonder why "memory" is in quotes. The answer is simple: you pass the "memory" to the CGI::SecureState object when it is initialized. So, to have a script that remembers everything except the parameters "foo" and "bar", do

    my $cgi = new CGI::SecureState(-mindSet => 'unforgetful',
                                   -memory => [qw(foo bar)]);

but to have a script that forgets everything except the parameters "user" and "pass", you would do instead

    my $cgi = new CGI::SecureState(-mindSet => 'forgetful',
                                   -memory => [qw(user pass)]);

Simple, really. In accord with the mindset of Perl, which is that methods should Do the Right Thing, the "forgetful" mindset will remember parameters when you tell it to, and not forget them until you force it to do so. This means that if you have a script to handle logins, like

    my $cgi = new CGI::SecureState(-mindSet => 'forgetful',
                                   -memory => [qw(user pass)]);

then other scripts do not have to re-memorize the "user" and "pass" parameters; a mere

    my $cgi = new CGI::SecureState(-mindSet => 'forgetful');
    my ($user,$pass) = ($cgi->param('user'),$cgi->param('pass'));

would suffice. However, had you read the rest of the documentation, that last line could even have been

    my ($user,$pass) = $cgi->params('user','pass');

Once you all see how more intuitive this new mindset is, I am sure that you will make the switch, but, in the meantime, the "unforgetful" mindset remains.

One more note about mindsets. In order to retain compatibility with older scripts, the "unforgetful" mindset will allow CGI parameters received from a client to overwrite previously saved parameters on disk. The new "forgetful" mindset discards parameters from clients if they already exist on disk. If you want to instead look at what the client sent you, then look at the section entitled "Recent Memory".

RECENT MEMORY

Most of you know that we as humans have two types of memory: short term and long term. Short term memory is useful if you only need the information for a short while and can then forget it (as in studying before a final exam). Long term memory is useful for things that stick around, like knowing how to ride a bicycle.

There are also two types of persistent data that a CGI application needs to store. The first type covers data that is used a few times and then forgotten, such as parameters passed to a search engine that displays its results over multiple pages (known as page-state). The second type covers data that is mostly static throughout the application, like a username and password (known as application-state). Coincidence? Perhaps.

Fortunately, CGI::SecureState now supports both. For purely short term data, you can use the user_* functions to replace the ones you would normally use. The user_* functions are so named to remind you that parameters that the user passes will override corresponding parameters already in short term memory. An extra feature is that they will fall back to the normal functions (param(), etc.) if you are requesting a parameter that is not in short term memory.

This means that you can now say:

   my $cgi = new CGI::SecureState(-mindSet => 'forgetful',
                                  -shortTerm => [qw(query type)]);

   my ($query, $type) = $cgi->user_params(qw(query type));
   my $next_page_url = $cgi->memory_as('url').";page=2";

and things will work out nicely. Now, you could have used long term memory to do the same thing, but you would be in for a nasty shock when the back button failed to work properly. For example, returning to the search engine, suppose a user searched for "marzipan" and then for "eggs". Realizing that marzipan is the more essential ingredient, the user backs up until he gets to the marzipan results and presses the "Next Page" link. Since the state file would store only the most recent search, the user recoils in horror as the "Next Page" is not filled with succulent almond pastries but instead white quasi-elliptical spheroids. Temporary memory does not have this problem, as it is not stored in the state file but tacked on as a special parameter list or a special sequence of hidden input fields when you use the memory_as() function. The only downside is, of course, that the temporary memory is not encrypted. This may be fixed in a future release of CGI::SecureState, but for now you will have to restrict sensitive information to long term memory only.

METHODS

After that lecture on script design, I am sure that you are hungering to know how to actually use this module. You will not be disappointed. CGI::SecureState inherits its methods from CGI.pm, overriding them as necessary:

new()

Creates a new CGI object and creates an associated encrypted state file if one does not already exist. new() has exactly one required argument (the mindset, of course!), and takes four optional arguments:

-mindSet

If the mindset is not specified, then CGI::SecureState will spit out nasty warnings until you change your scripts or set $CGI::SecureState::NASTY_WARNINGS to 0.

The mindset may be specified in a few different ways, the most common being to spell out 'forgetful' or 'unforgetful'. If it pleases you, you may also use '1' to specify forgetfulness, and '0' to specify unforgetfulness.

-memory

These are the parameters that you either want to persist between sessions (if you have a forgetful mindset), or those that you do not want to do so (if you have an unforgetful mindset). You may pass these parameters as a reference to an array. If you prefer the aliases "-longTerm" or "-longterm", you may use one of those instead.

-shortTerm

Also taking an array reference, this argument specifies the parameters that are not permanent enough for the state file but that you still want to keep around for a few requests. If you prefer the alias "-temp", you may use that instead.

-key

If you are concerned about the quality of the random data generated by multiple calls to rand(), then you can pass some better data along with this argument.

-errorSub

If you do not like the default error pages, then you may pass a reference to a subroutine that prints them out how you like them. The subroutine should print out a complete web page and include the "Content-Type" header. The possible errors that can be caught by the subroutine are:

    failed to open the state file
    failed to lock the state file
    failed to unlock the state file
    failed to close the state file
    failed to delete the state file
    invalid state file
    statefile inconsistent with mindset
    symlink encountered

If the subroutine can handle the error, it should return a true value, otherwise it should return false.

Examples:

    #forget everything but the "user" and "pass" params.
    $cgi = new CGI::SecureState(-mindSet => 'forgetful',
                                -memory => [qw(user pass)]);


    #invoke the old behavior of CGI::SecureState
    $cgi = new CGI::SecureState(-mindSet => 'unforgetful');
    $cgi = new CGI::SecureState(-mindSet => 0); #same thing

    #full listing
    $cgi = new CGI::SecureState(-stateDir => $statedir,
                                -mindSet => $mindset,
                                -memory => \@memory,
                                -shortTerm => \@temp_memory,
                                -errorSub => \&errorSub,
                                -key => $key);

    #if you don't like my capitalizations, then try
    $cgi = new CGI::SecureState(-statedir => $statedir,
                                -mindset => $mindset,
                                -memory => \@memory,
                                -shortterm => \@temp_memory,
                                -errorsub => \&errorSub,
                                -key => $key);

    #if you prefer the straight argument style (note absence of
    #errorSub -- it is only supported with the new argument style)
    $cgi = new CGI::SecureState($statedir, $mindset, \@memory,
                                \@temp_memory, $key);

    #cause nasty warnings by not specifying the mindset
    $cgi = new CGI::SecureState($statedir);
state_url()

Returns the URL of the current script with the state identification string. This URL should be used for referring to the stateful session associated with the query. Do NOT use this as the action of a form; see the state_field() function instead. Note that this does not include the short term memory; see the memory_as() function to do that.

state_param()

Returns a key-value pair that you can use to retain the session when linking to other scripts. If, for example, you want the script "other.pl" to be able to see your current script's session, you would use

    print "<a href=\"other.pl?",$cgi->state_param,
           "\">Click Here!</a>";

to do so. Note that this does not include the short term memory; see the memory_as() function to do that.

state_field()

Returns a hidden INPUT type for inclusion in HTML forms. Like state_url(), this element is used in forms to refer to the stateful session associated with the query. Note that this does not include the short term memory; see the memory_as() function to do that.

memory_as()

This allows you to get a state url/parameter/field with the short term memory attached. So, for example, if you wanted to retain short term memory between invocations of your script, you would write $cgi->memory_as('url') instead of $cgi->state_url. You can also write $cgi->memory_as('param') and $cgi->memory_as('field') instead of $cgi->state_param and $cgi->state_field.

params()

Allows you to get the scalar values of multiple parameters at once.

    my ($user,$pass) = $cgi->params(qw(user pass));

is equivalent to

    my ($user,$pass) = (scalar $cgi->param('user'),
                        scalar $cgi->param('pass'));
user_param()

Allows you to get (and set) a parameter in short term memory. If it cannot find the parameter you want to retrieve in short term memory, it will fall back to the normal param() call to get it for you. Setting parameters via this function will automatically add them to short term memory if they do not already exist. The interface is exactly the same as that of the ordinary param() call, except you can set more than one parameter at a time by passing names of parameters followed by array references, as you can with add().

user_params()

This function is analogous to params() except that it uses user_param() instead of param() to fetch multiple values for you.

add()

This command adds a new parameter to the CGI object and stores it to disk. Use this command if you want something to be saved, since the param() method will only temporarily set a parameter. add() uses the same syntax as param(), but you may also add more than one parameter at once if the values are in a reference to an array:

    $cgi->add(param_a => ['value'], param_b => ['value1', 'value2']);
remember()

This command is similar to add(), but saves current parameters to disk instead of new ones. For example, if "foo" and "bar" were passed in by the user and were not previously stored on disk,

    $cgi->remember('foo','bar');

will save their values to the state file. Use the add() method instead if you also want to set a new value for the parameter.

delete()

delete() is an overridden method that deletes named attributes from the query. The state file on disk is updated to reflect the removal of the parameter. Note that this has changed to accept a list of params to delete because otherwise the state file would be separately rewritten for each delete().

Important note: Attributes that are NOT explicitly delete()ed will lurk about and come back to haunt you unless you use the 'forgetful' mindset!

user_delete()

This function deletes values only from the short term memory, and has the same syntax as the overridden delete().

delete_all()

This command toasts all the current cgi parameters, but it merely clears the state file instead of deleting it. For that, use delete_session() instead.

delete_session()

This command not only deletes all the cgi parameters, but kills the disk image of the session as well. This method should be used when you want to irrevocably destroy a session.

age()

This returns the time in days since the session was last accessed.

clean_statedir()

Over time, if you are not careful, a buildup of stale state files may occur. You should use this call to clean them up, especially in logout scripts or cron jobs, where performance is not the most critical issue. This function optionally takes two arguments: a maximum idle time (in days) beyond which state files are deleted, and a directory to clean. The default behavior is to clean the current state directory of any state files that have been idle for more than an hour. You may also name the arguments using the '-age' and '-directory' attributes if you want to specify things out-of-order (like $cgi-clean_statedir(-directory => "foo", -age => 1/2);>).

GLOBALS

You may set these options to globally affect the behavior of CGI::SecureState.

NASTY_WARNINGS

Set this to 0 if you want warnings about deprecated behavior to be suppressed. This is especially true if you want to be left in peace while updating scripts based on older versions of CGI::SecureState. However, the warnings issued should be heeded because they generally result in better coding style and program security.

You may either do use CGI::SecureState qw(:no_nasty_warnings); #or $CGI::SecureState::NASTY_WARNINGS = 0;

Set this to 0 if you don't want CGI::SecureState to test for the presence of a symlink before writing to a state file. If this is set to 1 and CGI::SecureState sees a symlink in place of a real file, it will spit out a fatal error. It is generally a good idea to keep this in place, but if you have a good reason to, then do use CGI::SecureState qw(:dont_avoid_symlinks); #or $CGI::SecureState::AVOID_SYMLINKS = 1;

USE_FLOCK

Set this to 0 if you do not want CGI::SecureState to use "flock" to assure that only one instance of CGI::SecureState is accessing the state file at a time. Leave this at 1 unless you really have a good reason not to.

For users running a version of Windows NT (including 2000 and XP), you should set this variable to 1 because $^O will always report "MSWin32", regardless of whether your system is Win9x (which does not support flock) or WinNT (which does).

To set to 0, do use CGI::SecureState qw(:no_flock); #or $CGI::SecureState::USE_FLOCK = 0;

To set to 1, do use CGI::SecureState qw(:use_flock); #or $CGI::SecureState::USE_FLOCK = 1;

Extra and Paranoid Security

If the standard security is not enough, CGI::SecureState provides extra security by setting the appropriate options in CGI.pm. The ":extra_security" option enables private file uploads and sets the maximum size for a CGI POST to be 10 kilobytes. The ":paranoid_security" option disables file uploads entirely. To use them, do use CGI::SecureState qw(:extra_security); #or use CGI::SecureState qw(:paranoid_security);

To disable them, do use CGI::SecureState qw(:no_security); =back

EXAMPLES

There is now an official example of how to use CGI::SecureState in a large project. If that is what you are looking for, check out the Anthill Bug Manager at Sourceforge (http://anthillbm.sourceforge.net/).

This example is a simple log-in script. It should have a directory called "states" that it can write to.

  #!/usr/bin/perl -wT
  use CGI::SecureState qw(:paranoid_security);

  my $cgi = new CGI::SecureState(-stateDir => 'states',
                                 -mindSet => 'forgetful');

  my ($user,$pass,$lo)=$cgi->params(qw(user pass logout));
  my $failtime = $cgi->param('failtime') || 0;

  print $cgi->header();
  $cgi->start_html(-title => "CGI::SecureState Example");

  if ($user ne 'Cottleston' || $pass ne 'Pie') {
    if (defined $user) {
      $failtime+=$cgi->age()*86400;
      print "Incorrect Username/Password. It took you only ",
             $cgi->age*86400, " seconds to fail this time.";
      print " It has been $failtime seconds since you started.";
      $cgi->add(failtime => $failtime);
    }
    print $cgi->start_form(-action => $cgi->url());
    print $cgi->state_field();
    print "\n<b>Username: </b>", $cgi->textfield("user");
    print "\n<br><b>Password: </b>", $cgi->password_field("pass");
    print "<br>",$cgi->submit("Login"),$cgi->reset;
    print $cgi->end_form;
  } elsif (! defined $lo) {
    print "You logged in!\n<br>";
    print "Click <a href=\"",$cgi->url,"?",$cgi->state_param;
    print ";logout=true\">here</a> to logout.";
    $cgi->remember('user','pass');
  } else {
    print "You have logged out.";
    $cgi->delete_session;
  }
  print $cgi->end_html;

This example will show a form that will tell you what what previously entered. It should have a directory called "states" that it can write to.

  #!/usr/bin/perl -wT
  use CGI::SecureState qw(:paranoid_security);

  my $cgi = new CGI::SecureState(-stateDir => 'states',
                                -mindSet => 'unforgetful');
  print $cgi->header();
  $cgi->start_html(-title => "CGI::SecureState test",
                 -bgcolor => "white");
  print $cgi->start_form(-action => $cgi->url());
  print $cgi->state_field();
  print "\n<b>Enter some text: </b>";
  print $cgi->textfield("input","");
  print "<br>",$cgi->submit,$cgi->reset;
  print $cgi->end_form;
  print "\n<br><br><br>";

  unless (defined $cgi->param('num_inputs')) {
      $cgi->add('num_inputs' => '1');
  }
  else {
      $cgi->add('num_inputs' => ($cgi->param('num_inputs')+1));
  }
  $cgi->add('input'.$cgi->param('num_inputs') =>
          $cgi->param('input'));
  $cgi->delete('input');

  foreach ($cgi->param()) {
      print "\n<br>$_ -> ",$cgi->param($_) if (/input/);
  }
  print $cgi->end_html;

This example is a cron job that cleans up old state files in the directories /var/www/perl/states and /var/www/cgi-bin/states:

  #!/usr/bin/perl -w
  use CGI::SecureState;

  $cgi = new CGI::SecureState(-mindSet => 'forgetful',
                              -stateDir => '/var/www/perl/states');
  $cgi->cleanup_states;
  $cgi->cleanup_states(-directory => '/var/www/cgi-bin/states');
  $cgi->delete_session;

BUGS

There are no known bugs with the current version. However, take note of the limitations section.

If you do find a bug, you should send it immediately to behroozi@cpan.org with the subject "CGI::SecureState Bug". I am not responsible for problems in your code, so make sure that an example actually works before sending it. It is merely acceptable if you send me a bug report, it is better if you send a small chunk of code that points it out, and it is best if you send a patch--if the patch is good, you might see a release the next day on CPAN. Otherwise, it could take weeks . . .

LIMITATIONS

Crypt::Blowfish is the only cipher that CGI::SecureState is using at the moment. Change at your own risk.

CGI.pm has a tendency to set default values for form input fields that CGI::SecureState does NOT override. If this becomes problematic, use the -override setting when calling things like hidden().

Changes have been made so that saving/recovering Unicode now appears to work (with Perl 5.8.0). This is still not guaranteed to work; if you have reports of problems or solutions, please let me know.

As far as threading is concerned, CGI::SecureState (the actual module) is thread-safe as long as you provide it with an absolute path to the state file directory or if you do not change working directories in mid-stream. This does not mean that it is necessarily safe to use CGI::SecureState in an application with threads, as thread-safety may be compromised by either Crypt::Blowfish or Digest::SHA1. Check these modules to make sure that they are thread-safe before proceeding to use CGI::SecureState in an application with threads.

Until I can do more tests, assume that there is precisely zero support for either threading or unicode. If you would like to report your own results, send me a note and I will see what I can do about them.

Many previous limitations of CGI::SecureState have been removed in the 0.3x series.

CGI::SecureState requires:

Long file names (at least 27 chars): needed to ensure session authenticity.

Crypt::Blowfish: it couldn't be called "Secure" without. At some point in the future, this requirement will be changed. Tested with versions 2.06, 2.09.

Digest::SHA1: for super-strong (160 bit) hashing of data. It is used in key generation and filename generation. Tested with versions 1.03, 2.01.

CGI.pm: it couldn't be called "CGI" without. Should not be a problem as it comes standard with Perl 5.004 and above. Tested with versions 2.56, 2.74, 2.79, 2.89.

Fcntl: for file flags that are portable (like LOCK_SH and LOCK_EX). Comes with Perl. Tested with version 1.03.

File::Spec: for concatenating directories and filenames in a portable way. Comes with Perl. Tested with version 0.82.

Perl: Hmmm. Tested with stable releases from v5.005_03 to v5.8.0. There may be several bugs induced by lower versions of Perl, which are not limited to the failure to compile, the failure to behave properly, or the mysterious absence of your favorite pair of lemming slippers. The author is exempt from wrongdoing and liability, especially if you decide to use CGI::SecureState with a version of Perl less than 5.005_03.

SEE ALSO

  CGI(3), CGI::Persistent(3)

AUTHORS

Peter Behroozi, behroozi@cpan.org

1 POD Error

The following errors were encountered while parsing the POD:

Around line 1029:

You forgot a '=back' before '=head1'