Skip to content

Commit

Permalink
added support for rotating secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Dec 19, 2013
1 parent 20435a3 commit 57e5129
Show file tree
Hide file tree
Showing 15 changed files with 76 additions and 56 deletions.
5 changes: 4 additions & 1 deletion Changes
@@ -1,5 +1,8 @@

4.63 2013-12-18
4.63 2013-12-19
- Deprecated Mojolicious::secret in favor of Mojolicious::secrets.
- Added support for rotating secrets.
- Added secrets method to Mojolicious.

4.62 2013-12-17
- Deprecated Mojo::URL::to_rel.
Expand Down
2 changes: 1 addition & 1 deletion lib/Mojo/UserAgent.pm
Expand Up @@ -711,7 +711,7 @@ Application server relative URLs will be processed with, defaults to a
L<Mojo::UserAgent::Server> object.
# Introspect
say $ua->server->app->secret;
say for $ua->server->app->secrets;
# Change log level
$ua->server->app->log->level('fatal');
Expand Down
33 changes: 24 additions & 9 deletions lib/Mojolicious.pm
Expand Up @@ -4,7 +4,7 @@ use Mojo::Base 'Mojo';
# "Fry: Shut up and take my money!"
use Carp 'croak';
use Mojo::Exception;
use Mojo::Util 'decamelize';
use Mojo::Util qw(decamelize deprecated);
use Mojolicious::Commands;
use Mojolicious::Controller;
use Mojolicious::Plugins;
Expand All @@ -28,14 +28,14 @@ has moniker => sub { decamelize ref shift };
has plugins => sub { Mojolicious::Plugins->new };
has renderer => sub { Mojolicious::Renderer->new };
has routes => sub { Mojolicious::Routes->new };
has secret => sub {
has secrets => sub {
my $self = shift;

# Warn developers about insecure default
$self->log->debug('Your secret passphrase needs to be changed!!!');

# Default to moniker
return $self->moniker;
return [$self->moniker];
};
has sessions => sub { Mojolicious::Sessions->new };
has static => sub { Mojolicious::Static->new };
Expand Down Expand Up @@ -143,7 +143,7 @@ sub handler {
# Embedded application
my $stash = {};
if (my $sub = $tx->can('stash')) { ($stash, $tx) = ($tx->$sub, $tx->tx) }
$stash->{'mojo.secret'} //= $self->secret;
$stash->{'mojo.secrets'} //= $self->secrets;

# Build default controller
my $defaults = $self->defaults;
Expand Down Expand Up @@ -181,6 +181,16 @@ sub plugin {
$self->plugins->register_plugin(shift, $self, @_);
}

# DEPRECATED in Top Hat!
sub secret {
deprecated
'Mojolicious::secret is DEPRECATED in favor of Mojolicious::secrets';
my $self = shift;
return $self->secrets->[0] unless @_;
$self->secrets->[0] = shift;
return $self;
}

sub start { shift->commands->run(@_ ? @_ : @ARGV) }

sub startup { }
Expand Down Expand Up @@ -438,15 +448,20 @@ startup method to define the url endpoints for your application.
# Add another namespace to load controllers from
push @{$app->routes->namespaces}, 'MyApp::Controller';
=head2 secret
=head2 secrets
my $secret = $app->secret;
$app = $app->secret('passw0rd');
my $secrets = $app->secrets;
$app = $app->secrets(['passw0rd']);
A secret passphrase used for signed cookies and the like, defaults to the
Secret passphrases used for signed cookies and the like, defaults to the
L</"moniker"> of this application, which is not very secure, so you should
change it!!! As long as you are using the insecure default there will be debug
messages in the log file reminding you to change your passphrase.
messages in the log file reminding you to change your passphrase. The first
passphrase is used to create new signatures and all of them for verification,
so you can have rotating passphrases for increased security.
# Rotate passphrases
$app->secrets(['new_passw0rd', 'old_passw0rd', 'very_old_passw0rd']);
=head2 sessions
Expand Down
31 changes: 16 additions & 15 deletions lib/Mojolicious/Controller.pm
Expand Up @@ -195,8 +195,8 @@ sub render_exception {
};
my $inline = $renderer->_bundled(
$mode eq 'development' ? 'exception.development' : 'exception');
return $self if $self->_fallbacks($options, 'exception', $inline);
$self->_fallbacks({%$options, format => 'html'}, 'exception', $inline);
return $self if $self->__fallbacks($options, 'exception', $inline);
$self->__fallbacks({%$options, format => 'html'}, 'exception', $inline);
return $self;
}

Expand All @@ -216,8 +216,8 @@ sub render_not_found {
= {template => "not_found.$mode", format => $format, status => 404};
my $inline = $renderer->_bundled(
$mode eq 'development' ? 'not_found.development' : 'not_found');
return $self if $self->_fallbacks($options, 'not_found', $inline);
$self->_fallbacks({%$options, format => 'html'}, 'not_found', $inline);
return $self if $self->__fallbacks($options, 'not_found', $inline);
$self->__fallbacks({%$options, format => 'html'}, 'not_found', $inline);
return $self;
}

Expand Down Expand Up @@ -324,9 +324,9 @@ sub signed_cookie {
my ($self, $name, $value, $options) = @_;

# Response cookie
my $secret = $self->stash->{'mojo.secret'};
my $secrets = $self->stash->{'mojo.secrets'};
return $self->cookie($name,
"$value--" . Mojo::Util::hmac_sha1_sum($value, $secret), $options)
"$value--" . Mojo::Util::hmac_sha1_sum($value, $secrets->[0]), $options)
if defined $value;

# Request cookies
Expand All @@ -335,20 +335,13 @@ sub signed_cookie {

# Check signature
if ($value =~ s/--([^\-]+)$//) {
my $sig = $1;

# Verified
my $check = Mojo::Util::hmac_sha1_sum $value, $secret;
if (Mojo::Util::secure_compare $sig, $check) { push @results, $value }

# Bad cookie
if (__signature($value, $1, @$secrets)) { push @results, $value }
else {
$self->app->log->debug(
qq{Bad signed cookie "$name", possible hacking attempt.});
}
}

# Not signed
else { $self->app->log->debug(qq{Cookie "$name" not signed.}) }
}

Expand Down Expand Up @@ -446,7 +439,7 @@ sub write_chunk {
return $self->rendered;
}

sub _fallbacks {
sub __fallbacks {
my ($self, $options, $template, $inline) = @_;

# Mode specific template
Expand All @@ -462,6 +455,14 @@ sub _fallbacks {
return $self->render_maybe(%$options, inline => $inline, handler => 'ep');
}

sub __signature {
my ($value, $signature) = (shift, shift);
Mojo::Util::secure_compare($signature, Mojo::Util::hmac_sha1_sum($value, $_))
and return 1
for @_;
return undef;
}

1;

=encoding utf8
Expand Down
15 changes: 6 additions & 9 deletions lib/Mojolicious/Guides/Cookbook.pod
Expand Up @@ -1105,23 +1105,20 @@ that they will be picked up automatically by the command line interface?
sub run {
my ($self, $target) = @_;

# Leak secret passphrase
if ($target eq 'secret') {
my $secret = $self->app->secret;
say qq{The secret of this application is "$secret".};
}
# Leak secret passphrases
say for $self->app->secrets if $target eq 'secrets';
}

1;

There are many more useful attributes and methods in L<Mojolicious::Command>
that you can use or overload.

$ mojo spy secret
The secret of this application is "HelloWorld".
$ mojo spy secrets
HelloWorld

$ ./myapp.pl spy secret
The secret of this application is "secr3t".
$ ./myapp.pl spy secrets
secr3t

And to make your commands application specific, just put them in a different
namespace.
Expand Down
6 changes: 3 additions & 3 deletions lib/Mojolicious/Guides/FAQ.pod
Expand Up @@ -105,12 +105,12 @@ to use the MOJO_REACTOR environment variable to enforce a more portable one.

=head2 What does "Your secret passphrase needs to be changed" mean?

L<Mojolicious> uses a secret passphrase for security features such as signed
L<Mojolicious> uses secret passphrases for security features such as signed
cookies. It defaults to the moniker of your application, which is not very
secure, so we added this log message as a reminder. You can change the
passphrase with the attribute L<Mojolicious/"secret">.
passphrase with the attribute L<Mojolicious/"secrets">.

app->secret('My very secret passphrase.');
app->secrets(['My very secret passphrase.']);

=head2 What does "Nothing has been rendered, expecting delayed response" mean?

Expand Down
10 changes: 5 additions & 5 deletions lib/Mojolicious/Guides/Growing.pod
Expand Up @@ -349,9 +349,9 @@ Or perform quick requests right from the command line.
Sessions in L<Mojolicious> pretty much just work out of the box once you start
using the method L<Mojolicious::Controller/"session">, there is no setup
required, but we suggest setting a more secure passphrase with
L<Mojolicious/"secret">.
L<Mojolicious/"secrets">.

app->secret('Mojolicious rocks');
app->secrets(['Mojolicious rocks']);

This passphrase is used by the C<HMAC-SHA1> algorithm to make signed cookies
secure and can be changed at any time to invalidate all existing sessions.
Expand Down Expand Up @@ -392,7 +392,7 @@ like this.
use MyUsers;

# Make signed cookies secure
app->secret('Mojolicious rocks');
app->secrets(['Mojolicious rocks']);

helper users => sub { state $users = MyUsers->new };

Expand Down Expand Up @@ -513,7 +513,7 @@ actual action code needs to be changed.
sub startup {
my $self = shift;

$self->secret('Mojolicious rocks');
$self->secrets(['Mojolicious rocks']);
$self->helper(users => sub { state $users = MyUsers->new });

my $r = $self->routes;
Expand Down Expand Up @@ -620,7 +620,7 @@ information.
sub startup {
my $self = shift;

$self->secret('Mojolicious rocks');
$self->secrets(['Mojolicious rocks']);
$self->helper(users => sub { state $users = MyUsers->new });

my $r = $self->routes;
Expand Down
4 changes: 2 additions & 2 deletions lib/Mojolicious/Lite.pm
Expand Up @@ -792,10 +792,10 @@ session data gets serialized with L<Mojo::JSON>.
@@ counter.html.ep
Counter: <%= session 'counter' %>
Note that you should use a custom L<Mojolicious/"secret"> to make signed
Note that you should use custom L<Mojolicious/"secrets"> to make signed
cookies really secure.
app->secret('My secret passphrase here');
app->secrets(['My secret passphrase here']);
=head2 File uploads
Expand Down
4 changes: 2 additions & 2 deletions lib/Mojolicious/Plugin/DefaultHelpers.pm
Expand Up @@ -59,7 +59,7 @@ sub _content_for {
sub _csrf_token {
my $self = shift;
$self->session->{csrf_token}
||= sha1_sum($self->app->secret . steady_time . rand 999);
||= sha1_sum($self->app->secrets->[0] . steady_time . rand 999);
}

sub _current_route {
Expand Down Expand Up @@ -119,7 +119,7 @@ L<Mojolicious::Plugin::DefaultHelpers> implements the following helpers.
=head2 app
%= app->secret
%= app->secrets->[0]
Alias for L<Mojolicious::Controller/"app">.
Expand Down
2 changes: 1 addition & 1 deletion t/mojolicious/app.t
Expand Up @@ -59,7 +59,7 @@ is $t->app, $t->app->commands->app, 'applications are equal';
is $t->app->static->file('hello.txt')->slurp,
"Hello Mojo from a development static file!\n", 'right content';
is $t->app->moniker, 'mojolicious_test', 'right moniker';
is $t->app->secret, $t->app->moniker, 'secret defaults to moniker';
is $t->app->secrets->[0], $t->app->moniker, 'secret defaults to moniker';

# Missing methods and functions (AUTOLOAD)
eval { $t->app->missing };
Expand Down
2 changes: 1 addition & 1 deletion t/mojolicious/embedded_app.t
Expand Up @@ -11,7 +11,7 @@ use Mojolicious::Lite;
use Test::Mojo;

# Custom secret
app->secret('very secr3t!');
app->secrets(['very secr3t!']);

# Mount full external application a few times
use FindBin;
Expand Down
10 changes: 7 additions & 3 deletions t/mojolicious/group_lite_app.t
Expand Up @@ -11,6 +11,8 @@ use Mojo::UserAgent::CookieJar;
use Mojolicious::Lite;
use Test::Mojo;

app->secrets(['test1']);

get '/expiration' => sub {
my $self = shift;
if ($self->param('redirect')) {
Expand Down Expand Up @@ -262,7 +264,7 @@ $t->app->log->unsubscribe(message => $cb);
# Broken session cookie
$t->reset_session;
my $session = b("☃☃☃☃☃")->encode->b64_encode('');
my $hmac = $session->clone->hmac_sha1_sum($t->app->secret);
my $hmac = $session->clone->hmac_sha1_sum($t->app->secrets->[0]);
$t->get_ok('/bridge2stash' => {Cookie => "mojolicious=$session--$hmac"})
->status_is(200)->content_is("stash too!!!!!!!!\n");

Expand Down Expand Up @@ -291,14 +293,16 @@ $t->get_ok('/bridge2stash')->status_is(200)
->content_is(
"stash too!cookie!!signed_cookie!!bad_cookie--12345678!session!flash!\n");

# With cookies and session but no flash
# With cookies and session but no flash (rotating secrets)
$t->app->secrets(['test2', 'test1']);
$t->get_ok('/bridge2stash' => {'X-Flash2' => 1})->status_is(200)
->content_is(
"stash too!cookie!!signed_cookie!!bad_cookie--12345678!session!!\n");
ok $t->tx->res->cookie('mojolicious')->expires->epoch < time,
'session cookie expires';

# With cookies and session cleared
# With cookies and session cleared (rotating secrets)
$t->app->secrets(['test3', 'test2']);
$t->get_ok('/bridge2stash')->status_is(200)
->content_is("stash too!cookie!!signed_cookie!!bad_cookie--12345678!!!\n");

Expand Down
2 changes: 1 addition & 1 deletion t/mojolicious/lite_app.t
Expand Up @@ -445,7 +445,7 @@ is $t->app, app->commands->app, 'applications are equal';
is $t->app->moniker, 'lite_app', 'right moniker';
my $log = '';
my $cb = $t->app->log->on(message => sub { $log .= pop });
is $t->app->secret, $t->app->moniker, 'secret defaults to moniker';
is $t->app->secrets->[0], $t->app->moniker, 'secret defaults to moniker';
like $log, qr/Your secret passphrase needs to be changed!!!/, 'right message';
$t->app->log->unsubscribe(message => $cb);

Expand Down
4 changes: 2 additions & 2 deletions t/mojolicious/ojo.t
Expand Up @@ -12,8 +12,8 @@ use ojo;

# Application
a('/' => sub { $_->render(data => $_->req->method . $_->req->body) })
->secret('foobarbaz');
is a->secret, 'foobarbaz', 'right secret';
->secrets(['foobarbaz']);
is a->secrets->[0], 'foobarbaz', 'right secret';

# Requests
is g('/')->body, 'GET', 'right content';
Expand Down
2 changes: 1 addition & 1 deletion t/pod_coverage.t
Expand Up @@ -10,7 +10,7 @@ plan skip_all => 'Test::Pod::Coverage 1.04 required for this test!'
# DEPRECATED in Top Hat!
my @tophat = (
qw(app app_url detect_proxy http_proxy https_proxy name need_proxy new),
qw(no_proxy to_rel)
qw(no_proxy secret to_rel)
);

# False positive constants
Expand Down

0 comments on commit 57e5129

Please sign in to comment.