Skip to content

Commit

Permalink
Applied security patch from Cees. Updated tests to verify the changes…
Browse files Browse the repository at this point in the history
… the patch made.
  • Loading branch information
cromedome committed Jan 5, 2011
1 parent 757507b commit 9acb5b6
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 23 deletions.
33 changes: 26 additions & 7 deletions README
Expand Up @@ -72,13 +72,25 @@ DESCRIPTION

When a CAPTCHA is created with this module, raw image data is
transmitted from your web application to the client browser. A cookie
containing an encrypted hash is also transmitted with the image. When
the client submits their form for processing (along with their
verification of the random string), "captcha_verify()" encrypts the
verification string with the same salt used to encrypt the hash sent in
the cookie. If the newly encrypted string matches the original encrypted
hash, we trust that the CAPTCHA has been successfully entered, and we
allow the user to continue processing their form.
containing a checksum is also transmitted with the image. When the
client submits their form for processing (along with their verification
of the random string), "captcha_verify()" generates a checksum of the
verification string the user entered. If the newly generated checksum
matches the checksum found in the cookie, we trust that the CAPTCHA has
been successfully entered, and we allow the user to continue processing
their form.

The checksum is generated by taking the string in question, and joining
it with a SECRET. We then generate an SHA1 hex digest of the resulting
string. The end user will not be able to generate their own checksums to
bypass the CAPTCHA check, because they do not know the value of our
SECRET. This means it is important to choose a good value for your
SECRET.

An easy way to generate a relatively good secret is to run the following
perl snippet:

perl -MDigest::SHA1=sha1_base64 -le 'print sha1_base64($$,time(),rand(9999))'

The author recognizes that the transmission of a cookie with the CAPTCHA
image may not be a popular decision, and welcomes any patches from those
Expand Down Expand Up @@ -111,6 +123,10 @@ FUNCTIONS
GD::SecurityImage. Please see the documentation for GD::SecurityImage
for more information.

SECRET
This specifies the secret that will be used when generating the checksum
hash.

captcha_create()
Creates the CAPTCHA image, and return a cookie with the encrypted hash
of the random string. Takes no arguments.
Expand Down Expand Up @@ -153,6 +169,9 @@ ACKNOWLEDGEMENTS
Tony Fraser for all of their valuable input, and to the rest who
contributed ideas and criticisms on the CGI::Application mailing list.

Additional thanks to chorny and Cees for the various bug fixes and
patches they have submitted.

SEE ALSO
CGI::Application GD::SecurityImage Wikipedia entry for CAPTCHA -
<http://en.wikipedia.org/wiki/Captcha>
Expand Down
62 changes: 48 additions & 14 deletions lib/CGI/Application/Plugin/CAPTCHA.pm
Expand Up @@ -3,6 +3,7 @@ package CGI::Application::Plugin::CAPTCHA;
use strict;

use GD::SecurityImage;
use Digest::SHA1;
use vars qw($VERSION @EXPORT);

require Exporter;
Expand Down Expand Up @@ -96,13 +97,24 @@ makes it more convenient to access L<GD::SecurityImage> functionality, and
gives a more L<CGI::Application>-like way of doing it.
When a CAPTCHA is created with this module, raw image data is transmitted from
your web application to the client browser. A cookie containing an encrypted
hash is also transmitted with the image. When the client submits their form
for processing (along with their verification of the random string),
C<captcha_verify()> encrypts the verification string with the same salt used
to encrypt the hash sent in the cookie. If the newly encrypted string matches
the original encrypted hash, we trust that the CAPTCHA has been successfully
entered, and we allow the user to continue processing their form.
your web application to the client browser. A cookie containing a checksum is
also transmitted with the image. When the client submits their form for
processing (along with their verification of the random string),
C<captcha_verify()> generates a checksum of the verification string the user
entered. If the newly generated checksum matches the checksum found in the
cookie, we trust that the CAPTCHA has been successfully entered, and we allow
the user to continue processing their form.
The checksum is generated by taking the string in question, and joining it with
a SECRET. We then generate an SHA1 hex digest of the resulting string. The
end user will not be able to generate their own checksums to bypass the CAPTCHA
check, because they do not know the value of our SECRET. This means it is
important to choose a good value for your SECRET.
An easy way to generate a relatively good secret is to run the following perl
snippet:
perl -MDigest::SHA1=sha1_base64 -le 'print sha1_base64($$,time(),rand(9999))'
The author recognizes that the transmission of a cookie with the CAPTCHA image
may not be a popular decision, and welcomes any patches from those who can
Expand Down Expand Up @@ -140,6 +152,10 @@ This specifies what options will be passed to the C<particle()> method of
L<GD::SecurityImage>. Please see the documentation for L<GD::SecurityImage>
for more information.
=head3 SECRET
This specifies the secret that will be used when generating the checksum hash.
=cut

sub captcha_config
Expand Down Expand Up @@ -180,6 +196,13 @@ sub captcha_config
$self->{__CAP__CAPTCHA_CONFIG}->{PARTICLE_OPTIONS} = delete $props->{PARTICLE_OPTIONS};
}

# Check for SECRET
if ($props->{SECRET})
{
die "captcha_config() error: parameter SECRET is not a string" if ref $props->{SECRET};
$self->{__CAP__CAPTCHA_CONFIG}->{SECRET} = delete $props->{SECRET};
}

# Check for DEBUG
if ($props->{DEBUG})
{
Expand Down Expand Up @@ -210,7 +233,8 @@ sub captcha_create
my %image_options = %{ $self->{__CAP__CAPTCHA_CONFIG}->{ IMAGE_OPTIONS } };
my @create_options = @{ $self->{__CAP__CAPTCHA_CONFIG}->{ CREATE_OPTIONS } };
my @particle_options = @{ $self->{__CAP__CAPTCHA_CONFIG}->{ PARTICLE_OPTIONS } };
my $debug = $self->{__CAP__CAPTCHA_CONFIG}->{ DEBUG } ;
my $secret = $self->{__CAP__CAPTCHA_CONFIG}->{ SECRET } ;
my $debug = $self->{__CAP__CAPTCHA_CONFIG}->{ DEBUG } ;

# Create the CAPTCHA image
my $image = GD::SecurityImage->new( %image_options );
Expand All @@ -219,16 +243,20 @@ sub captcha_create
$image->particle( @particle_options );
my ( $image_data, $mime_type, $random_string ) = $image->out;

# check the secret
if (!$secret) {
$secret = Digest::SHA1::sha1_base64( ref $self );
warn "using default SECRET! Please provide a proper SECRET when using the CGI::Application::Plugin::CAPTCHA plugin";
}

# Create the verification hash
use Data::Random qw(:all);
my $salt = rand_chars( set => 'alphanumeric', size => 6);
my $hash = crypt($random_string, $salt);
my $hash = Digest::SHA1::sha1_base64(join("\0", $secret, $random_string));

# Stuff the verification hash in a cookie and push it out to the
# client.
my $cookie = $self->query->cookie("hash" => $hash);
$self->header_type ( 'header' );
$self->header_props( -type => $mime_type, -cookie => [ $cookie ] );
$self->header_props( -type => $mime_type, -cookie => [ $cookie ], -expires => '-1d', '-cache-control' => 'no-cache', -pragma => 'no-cache' );
return $image_data;
}

Expand All @@ -245,9 +273,15 @@ returns false.
sub captcha_verify
{
my ($self, $hash, $verify) = @_;
my $secret = $self->{__CAP__CAPTCHA_CONFIG}->{ SECRET } ;

# check the secret
if (!$secret) {
$secret = Digest::SHA1::sha1_base64( ref $self );
warn "using default SECRET! Please provide a proper SECRET when using the CGI::Application::Plugin::CAPTCHA plugin";
}

my $salt = substr($hash, 0, 2);
return 1 if crypt($verify, $salt) eq $hash;
return 1 if Digest::SHA1::sha1_base64(join("\0", $secret, $verify)) eq $hash;
return 0;
}

Expand Down
1 change: 1 addition & 0 deletions t/01-config.t
Expand Up @@ -26,6 +26,7 @@ CONFIG_TESTING:
},
CREATE_OPTIONS => [ 'normal', 'rect' ],
PARTICLE_OPTIONS => [ 300 ],
SECRET => 'vbCrfzMCi45TD7Uz4C6fjWvX6us',
);

ok($app->{__CAP__CAPTCHA_CONFIG}->{IMAGE_OPTIONS}, "IMAGE_OPTIONS defined" );
Expand Down
7 changes: 5 additions & 2 deletions t/03-verify.t
Expand Up @@ -48,8 +48,11 @@ CREATE_TESTING:
isnt($hash, "", "Received cryptographic hash in cookie");

# Make sure our cookie contains the CAPTCHA value ABC123
my $salt = substr($hash, 0, 2);
ok(crypt("ABC123", $salt) eq $hash, "Hash contains CAPTCHA value ABC123");
my $secret = 'vbCrfzMCi45TD7Uz4C6fjWvX6us';
my $check = Digest::SHA1::sha1_base64(join("\0", $secret, 'ABC123'));
#my $salt = substr($hash, 0, 2);
#ok(crypt("ABC123", $salt) eq $hash, "Hash contains CAPTCHA value ABC123");
ok( $check eq $hash, "Hash contains CAPTCHA value ABC123");

# Make sure captcha_verify() actually verifies!
my $app = TestApp2->new();
Expand Down
1 change: 1 addition & 0 deletions t/TestApp2.pm
Expand Up @@ -20,6 +20,7 @@ sub setup
},
CREATE_OPTIONS => [ 'normal', 'rect' ],
PARTICLE_OPTIONS => [ 300 ],
SECRET => 'vbCrfzMCi45TD7Uz4C6fjWvX6us',
DEBUG => 1,
);
}
Expand Down

0 comments on commit 9acb5b6

Please sign in to comment.