[cgiapp] CAP::Security::CSRF -- useful?

Mark Rajcok mrajcok at gmail.com
Sat Nov 28 18:21:13 EST 2009


I want to guard against multiple form submissions (reload, resubmit, etc.)
and CSRF in my app:
  http://en.wikipedia.org/wiki/Cross-site_request_forgery
  http://www.perlmonks.org/?node_id=606832

I hacked up a simple plugin. Do you think it would be useful? or might
something like this already exist?

package CGI::Application::Plugin::Security::CSRF;
use strict;
use warnings;
use base 'Exporter';
use CGI::Application;
use CGI::Application::Plugin::Session;
use Digest::SHA1 qw(sha1_base64);

use vars qw($VERSION @EXPORT);
@EXPORT = qw(csrf_insert_token csrf_attack);
$VERSION = '1.0';

sub csrf_insert_token {
    my ($self, $token_name, $tmpl) = @_;
    if($self->query->param('csrf_token')) {
        # reuse the token that was submitted with the form; don't generate a
new one
    } else {
        # generate a new token for this new form
        my $token = sha1_base64($self->session->param('_SESSION_ID') .
time);
        # TBD: below, what if the app is not using HTML::Template ??
        $tmpl->param(csrf_token => $token);
        $self->session->param($token_name => $token);
    }
}
sub csrf_attack {
    my ($self, $token_name) = @_;
    if( ($self->query->request_method ne 'POST')   # ensure the form was
POSTed
     || ($self->session->param($token_name) ne
$self->query->param('csrf_token')) ) {
         return 1
    }
    $self->session->clear($token_name);
    return 0
}
1;

Example usage:

sub edit_account_form : Runmode {
    my $self = shift;
    my $errs = shift; # created by edit_check()
    my $t = $self->load_tmpl('account_form.html');
...
    $t->param(rm => 'edit_check');
    $self->csrf_insert_token('edit_account_token', $t);   <<-------------
    $t->param($errs) if $errs;
    $t->output
}
#------------------------------------------------------------------------------
sub edit_check : Runmode {
    my $self = shift;
    my $dfv_results = $self->check_rm('edit_account_form',
my_dfv_edit_rules) ||
        return $self->check_rm_error_page;
    if($self->csrf_attack('edit_acount_token')) {     <<------------------
        return $self->custom_error_page(msg =>
             'It seems you may have been performing a similar operation in
multiple windows'
            .' (which is not supported for security reasons),'
            .' or you tried to submit or reload the same form twice.');
           # or there was a real CSRF attack attempt, in which case no
           # one will ever see this error message
    }
    ...

Each HTML form that uses this plugin will need the following hidden field:
    <input type="hidden" name="csrf_token" value="<tmpl_var token>" />

Current limitations:

1. csrf_insert_token() assumes/requires HTML::Template.  Can a plugin
determine if a different template system is in use?
If so, the $tmpl argument could be optional, and if it is not present, the
method could return the generated token.  The app developer would then need
to use whatever template system method is appropriated to add the token to
the form.

2. tokens are stored in the session object. A token is removed on a
successful form submission.  They remain otherwise.  So tokens could collect
over time, but only if the user visited form pages and did not submit them
successfully.  For apps that have a lot of forms that use this plugin, this
could be an issue.

3. As the error message implies in the edit_check() rm above, using a fixed
token name like 'edit_account_token' will not allow a user to have two
windows open to the same form and successfully submit both forms.  Only the
form that was opened last will succeed. For most apps, I would consider this
a feature, not a bug (i.e., the "older" window becomes stale/unusable). But
for certain apps, maybe this would be a problem.


More information about the cgiapp mailing list