Skip to content

Refuse to set signed or encrypted cookies with an insecure default secret #2252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions lib/Mojolicious/Controller.pm
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ sub encrypted_cookie {
my $app = $self->app;
my $secret = $app->secrets->[0];
my $moniker = $app->moniker;

Carp::croak 'Your secret passphrase must be changed to set encrypted cookies (see FAQ for more)'
if !$ENV{MOJO_ALLOW_INSECURE_SECRET} and (!length $secret or $secret eq $moniker);

return $self->cookie($name, Mojo::Util::encrypt_cookie($value, $secret, $moniker), $options);
}

Expand Down Expand Up @@ -242,12 +246,24 @@ sub session {
my $stash = $self->stash;
$self->app->sessions->load($self) unless exists $stash->{'mojo.active_session'};

# Hash
my $session = $stash->{'mojo.session'} //= {};

# Require changed secret if session data will be set
my $set_operation = !!(@_ > 1 || ref $_[0]);
Copy link
Preview

Copilot AI Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The use of double negation (!!) can reduce readability; consider using an explicit boolean expression to check for a set operation.

Suggested change
my $set_operation = !!(@_ > 1 || ref $_[0]);
my $set_operation = (@_ > 1 || ref $_[0]) ? 1 : 0;

Copilot uses AI. Check for mistakes.

if (!$ENV{MOJO_ALLOW_INSECURE_SECRET} and (keys %$session or $set_operation)) {
my $app = $self->app;
my $secret = $app->secrets->[0];
my $moniker = $app->moniker;

Carp::croak 'Your secret passphrase must be changed to set session data (see FAQ for more)'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these secrets are "passphrase"s (and the FAQ doesn't call them that either)

if !length $secret or $secret eq $moniker;
}

# Hash
return $session unless @_;

# Get
return $session->{$_[0]} unless @_ > 1 || ref $_[0];
return $session->{$_[0]} unless $set_operation;

# Set
my $values = ref $_[0] ? $_[0] : {@_};
Expand All @@ -262,8 +278,15 @@ sub signed_cookie {
# Request cookie
return $self->every_signed_cookie($name)->[-1] unless defined $value;

my $app = $self->app;
my $secret = $app->secrets->[0];
my $moniker = $app->moniker;

Carp::croak 'Your secret passphrase must be changed to set signed cookies (see FAQ for more)'
if !$ENV{MOJO_ALLOW_INSECURE_SECRET} and (!length $secret or $secret eq $moniker);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to move this check into $app->session?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no session method for Mojolicious; that would be calling the session helper, which just forwards to the session method on the controller which is handled above (albeit with some issues as described in the initial comment).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, sorry, github threw away my comment, and i crankly re-typed it wrong.

I mean should it be in $app->secrets

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That attribute may be accessed by applications that don't actually do anything with it, such as Mojolicious::Plugin::Mount. But perhaps we can work around that more easily than the problems with this approach.


# Response cookie
my $sum = Digest::SHA::hmac_sha256_hex("$name=$value", $self->app->secrets->[0]);
my $sum = Digest::SHA::hmac_sha256_hex("$name=$value", $secret);
return $self->cookie($name, "$value--$sum", $options);
}

Expand Down Expand Up @@ -443,6 +466,9 @@ the same name, and you want to access more than just the last one, you can use L
are encrypted with ChaCha20-Poly1305, to prevent tampering, and the ones failing decryption will be automatically
discarded. Note that this method is B<EXPERIMENTAL> and might change without warning!

The L<application secrets|Mojolicious/secrets> B<MUST> be changed from the default to a secure value to set
encrypted cookies. This requirement can be bypassed by setting the C<MOJO_ALLOW_INSECURE_SECRET> environment variable.

=head2 every_cookie

my $values = $c->every_cookie('foo');
Expand Down Expand Up @@ -745,6 +771,10 @@ Persistent data storage for the next few requests, all session data gets seriali
Base64 encoded in HMAC-SHA256 signed cookies, to prevent tampering. Note that cookies usually have a C<4096> byte
(4KiB) limit, depending on browser.

The L<application secrets|Mojolicious/secrets> B<MUST> be changed from the default to a secure value to set
session data in a signed or encrypted cookie. This requirement can be bypassed by setting the
C<MOJO_ALLOW_INSECURE_SECRET> environment variable.

# Manipulate session
$c->session->{foo} = 'bar';
my $foo = $c->session->{foo};
Expand All @@ -770,6 +800,9 @@ same name, and you want to access more than just the last one, you can use L</"e
cryptographically signed with HMAC-SHA256, to prevent tampering, and the ones failing signature verification will be
automatically discarded.

The L<application secrets|Mojolicious/secrets> B<MUST> be changed from the default to a secure value to set
signed cookies. This requirement can be bypassed by setting the C<MOJO_ALLOW_INSECURE_SECRET> environment variable.

=head2 stash

my $hash = $c->stash;
Expand Down
2 changes: 2 additions & 0 deletions t/mojolicious/app.t
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use Mojo::Date;
use Mojo::File qw(path);
use Mojo::Home;
use Mojo::IOLoop;
use Mojo::Util qw(generate_secret);
use Mojolicious;
use Mojolicious::Controller;

Expand Down Expand Up @@ -658,6 +659,7 @@ subtest 'Override deployment plugins' => sub {
};

$t = Test::Mojo->new('MojoliciousTest');
$t->app->secrets([generate_secret]);

# MojoliciousTestController::Foo::plugin_upper_case
$t->get_ok('/plugin/upper_case')
Expand Down
3 changes: 3 additions & 0 deletions t/mojolicious/embedded_lite_app.t
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use Test::Mojo;

package TestApp;
use Mojolicious::Lite;
use Mojo::Util qw(generate_secret);

app->secrets([generate_secret]);

get '/hello' => sub {
my $c = shift;
Expand Down
3 changes: 3 additions & 0 deletions t/mojolicious/longpolling_lite_app.t
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' }
use Test::Mojo;
use Test::More;
use Mojo::IOLoop;
use Mojo::Util qw(generate_secret);
use Mojolicious::Lite;

package MyTestApp::Controller;
Expand All @@ -14,6 +15,8 @@ sub DESTROY { shift->stash->{destroyed} = 1 }

package main;

app->secrets([generate_secret]);

app->controller_class('MyTestApp::Controller');

get '/write' => sub {
Expand Down
3 changes: 3 additions & 0 deletions t/mojolicious/rebased_lite_app.t
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' }
use Test::Mojo;
use Test::More;
use Mojo::URL;
use Mojo::Util qw(generate_secret);
use Mojolicious::Lite;

app->secrets([generate_secret]);

# Rebase hook
app->hook(
before_dispatch => sub {
Expand Down
17 changes: 17 additions & 0 deletions t/mojolicious/session_lite_app.t
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,21 @@ subtest 'Rotating secrets' => sub {
};
};

subtest 'Insecure secret' => sub {
subtest 'Insecure secret (signed cookie)' => sub {
$t->reset_session;
$t->app->secrets([app->moniker]);
Copy link
Preview

Copilot AI Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] For clarity and consistency with other parts of the code, consider using $t->app->moniker instead of app->moniker when setting insecure secrets in tests.

Suggested change
$t->app->secrets([app->moniker]);
$t->app->secrets([$t->app->moniker]);

Copilot uses AI. Check for mistakes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is actually good!

$t->app->sessions->encrypted(0);
$t->get_ok('/login')->status_is(500);
};

subtest 'Insecure secret (encrypted cookie)' => sub {
plan skip_all => 'CryptX required!' unless Mojo::Util->CRYPTX;
$t->reset_session;
$t->app->secrets([app->moniker]);
$t->app->sessions->encrypted(1);
$t->get_ok('/login')->status_is(500);
};
};

done_testing();
3 changes: 3 additions & 0 deletions t/mojolicious/validation_lite_app.t
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ use Test::Mojo;
use Test::More;
use Mojo::Asset::Memory;
use Mojo::Upload;
use Mojo::Util qw(generate_secret);
use Mojolicious::Lite;

app->secrets([generate_secret]);

# Custom check
app->validator->add_check(two => sub { length $_[2] == 2 ? undef : "e:$_[1]" });

Expand Down
Loading