Skip to content
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

Secure by default sessions #2200

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

stigtsp
Copy link

@stigtsp stigtsp commented Sep 27, 2024

Summary

To make session secrets secure by default, generate and persist a 256 bit session secret:

  • Add urandom_bytes and urandom_urlsafe to Mojo::Util for generating secure random bits from either Crypt::Random or /dev/urandom

  • Don't use the hard coded moniker as the default secret

  • Generate and store a strong secret if not exists in $ENV{MOJO_HOME}/mojo.secrets, overridable with $ENV{MOJO_SECRETS_FILE} when app->secrets is called

  • Only load secrets from mojo.secrets that are over 22 chars

  • Use urandom_urlsafe when generating CSRF tokens

  • Use urandom_urlsafe in mojo generate app

  • Add mojo generate secret

  • Tests:

    • Add misc tests for generating and loading mojo.secrets in t/mojolicious/secret/ and for mojo generate secret.
    • Add a default secret in t/mojolicious/mojo.secrets so other session checks work
  • Install Crypt::URandom in GH Windows workflow so urandom_bytes works on that platform

Also consider

Motivation

Making HMAC protected sessions secure by default after discussing with @jberger at PTS24.

It has been demonstrated by Baking Mojolicious cookies and the recent discussion in #1791 that this is not the case.

lib/Mojo/Util.pm Outdated Show resolved Hide resolved

$self->log->trace("Your secret passphrase has been set to strong random value and stored in $file");

return [$secret];
Copy link
Contributor

Choose a reason for hiding this comment

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

While I think this is generally a good and necessary step, this will invalidate session cookies in currently insecure apps so that will need to at minimum be noted in the changelog (which is not the responsibility of this PR).

lib/Mojolicious.pm Outdated Show resolved Hide resolved
lib/Mojolicious/Command/Author/generate/secret.pm Outdated Show resolved Hide resolved
t/mojo/util.t Outdated Show resolved Hide resolved
lib/Mojo/Util.pm Outdated Show resolved Hide resolved
lib/Mojo/Util.pm Outdated Show resolved Hide resolved
@kraih
Copy link
Member

kraih commented Sep 27, 2024

Make sure to squash your comits when you are ready.

lib/Mojo/Util.pm Outdated Show resolved Hide resolved
@stigtsp
Copy link
Author

stigtsp commented Sep 28, 2024

Thanks! I'm addressing all feedback, and will update the PR tomorrow or so.

lib/Mojo/Util.pm Outdated
require Win32::API;

my $RtlGenRandom = Win32::API->new('advapi32', 'INT SystemFunction036 (PVOID RandomBuffer, ULONG RandomBufferLength)')
or croak "SystemFunction036 import failed: $!";
Copy link
Member

Choose a reason for hiding this comment

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

Is this actually used in our Windows CI tests? This seems like code nobody on the team can maintain long term and we will end up removing it because of that the first it causes problems.

Copy link
Author

Choose a reason for hiding this comment

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

Tests are running SystemFunction036 (RtlGenRandom) on Windows to get random bytes. It's used in the same way as for example Crypt::URandom. However, It is a legacy API, so we can consider using ProcessPrng instead.

Here are some discussions in rust and golang:

Copy link
Member

Choose a reason for hiding this comment

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

This part of the code will probably end up being a blocker, since i don't see anyone on the team agreeing to take responsibility for maintaining it in the future.

Choose a reason for hiding this comment

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

I'd not expect finding such code in web framework. In the same time, AFAIK the alternatives are:

  • using some small old library with single maintainer and with no recent releases, that does exactly this call, but conveniently wrapped,
  • overkill of making Net-SSLeay a dependency to get it through openssl binding.

Copy link
Member

@kraih kraih Sep 30, 2024

Choose a reason for hiding this comment

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

Net::SSLeay is already a soft dependency, maybe there's a clean way to make this feature optional? A hard dependency is pretty much out of the question. It's unfortunate Perl doesn't ship with some kind of OpenSSL binding, for mojo.js we've chosen a different solution and just encrypt all sessions because Node has crypto support.

Copy link

@guest20 guest20 Sep 30, 2024

Choose a reason for hiding this comment

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

Crypt::URandom has a pile of different window support calls. Adding that as an optional dependency for windows enjoyers might unblock this, while outsourcing the windows support

Copy link
Author

@stigtsp stigtsp Sep 30, 2024

Choose a reason for hiding this comment

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

Crypt::URandom appears to sets up some context for CryptGenRandom for Win2k, and uses RtlGenRandom for later Windows versions.

According to a discussion in golang crypto/rand, it seems fine to use RtlGenRandom (SystemFunction036) golang/go#33542 (comment)

We could use Crypt::URandom as a soft dependency for Win and Linux/UNIX, and fail over to /dev/urandom if that module is not installed? (And fatal on Windows maybe)

Copy link
Author

@stigtsp stigtsp Sep 30, 2024

Choose a reason for hiding this comment

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

What about something like this?

use constant CRYPT_URANDOM => !!eval { require Crypt::URandom };

...

sub urandom_bytes {
  my $num   = shift || 32;

  return Crypt::URandom::urandom($num) if CRYPT_URANDOM;

  croak 'Cannot find /dev/urandom. On Windows? Try installing "Crypt::URandom" from CPAN' 
    unless -f '/dev/urandom';

  open(my $urandom, '<', '/dev/urandom')  or croak "Cannot open /dev/urandom: $!";
  sysread($urandom, my $bytes, $num) == $num or die "sysread() from /dev/urandom didn't return $num bytes";
  close($urandom);

  return $bytes;
}

Copy link

Choose a reason for hiding this comment

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

That's exactly the kind of shim(affectionate) I was suggesting!

You might not need to mention windows directly in the message, you know, just to be inclusive of that one person who runs mojo apps on VAX or AIX

Copy link
Author

Choose a reason for hiding this comment

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

I've updated the PR with logic similar to the above, avoiding any Win32 specific code.

Also added Crypt::URandom to the Windows GH actions workflow, should the be in Makefile.PL instead?

lib/Mojo/Util.pm Outdated Show resolved Hide resolved
lib/Mojolicious.pm Outdated Show resolved Hide resolved
lib/Mojolicious.pm Outdated Show resolved Hide resolved
@stigtsp stigtsp force-pushed the secure-by-default-sessions branch 3 times, most recently from c33c0eb to 6bd013a Compare September 29, 2024 13:52
@stigtsp
Copy link
Author

stigtsp commented Sep 29, 2024

I've (hopefully) addressed all comments and pushed a single commit to this PR.

One problem that remains is that a mojo.secrets file is left over in the t/mojo/ directory after tests are run, likely due to something calling app->secrets there. A quick fix for this is to copy the one from t/mojolicious/ but it seems a bit messy. Any better ideas on how to fix this?

lib/Mojo/Util.pm Outdated Show resolved Hide resolved
lib/Mojo/Util.pm Outdated Show resolved Hide resolved
lib/Mojolicious/Command/Author/generate/secret.pm Outdated Show resolved Hide resolved
Comment on lines +52 to +54
# Read secrets and filter out those who are less than 22 characters long
# (~128 bits), as they are not likely to be sufficiently strong.
my @secrets = grep { length $_ >= 22 } split /\n/, path($file)->slurp;
Copy link

Choose a reason for hiding this comment

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

Should there be a warning message if a candidate secret is skipped? Right now, they're discarded silently.

Copy link
Author

Choose a reason for hiding this comment

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

Yes - or maybe even a fatal error?

Btw, I'm not sure how useful more than one key in app->secrets is. With a strong secret, wouldn't rotation only makes sense if it is compromised/leaked/etc? And then you wouldn't want to continue trusting the old key imho.

Copy link
Contributor

Choose a reason for hiding this comment

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

It is useful so that instances using the session key can be upgraded to use the new secret over even a short period of time without immediately invalidating everything.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, regular secret rotation is good practice even when not found to be compromised.

Copy link

Choose a reason for hiding this comment

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

I think only the first secret (the one used to set new session cookies) should be subject to the length restriction, since you might still want to restore sessions cookies that were set with crappy secrets so you can re-issue them with new stronger key...

@stigtsp stigtsp force-pushed the secure-by-default-sessions branch 4 times, most recently from 0cbcf0d to 53b7107 Compare October 1, 2024 20:38
* Add `urandom_bytes` and `urandom_urlsafe` to `Mojo::Util` for
  generating secure random bits from either Crypt::Random or
  /dev/urandom

* Don't use the hard coded moniker as the default secret

* Generate and store a strong secret if not exists in
  `$ENV{MOJO_HOME}/mojo.secrets`, overridable with
  `$ENV{MOJO_SECRETS_FILE}` when app->secrets is called

* Only load secrets from `mojo.secrets` that are over 22 chars

* Use `urandom_urlsafe` when generating CSRF tokens

* Use `urandom_urlsafe` when in `mojo generate app`

* Add `mojo generate secret`

* Tests:

  - Add misc tests for generating and loading mojo.secrets in
    `t/mojolicious/secret/` and for `mojo generate secret`.

  - Add a default secret in `t/mojolicious/mojo.secrets` so other
    session checks work

* Install Crypt::URandom in GH Windows workflow so urandom_bytes works
  on that platform
@stigtsp stigtsp marked this pull request as ready for review October 2, 2024 05:48
@kraih
Copy link
Member

kraih commented Oct 10, 2024

Heads up, early November is SUSE hack week, and i might work on a port of the mojo.js encrypted session implementation to Mojolicious. That's probably the best solution for security.

@daleif
Copy link

daleif commented Oct 11, 2024 via email

@jkramarz
Copy link

Heads up, early November is SUSE hack week, and i might work on a port of the mojo.js encrypted session implementation to Mojolicious. That's probably the best solution for security.

Assuming that we're talking about https://github.com/mojolicious/mojo.js/blob/436b3263690efe65b0a8b4ecbbb09e4a3c99af5e/src/session.ts#L77 , please consider if it's not vulnerable to IV nonce reuse (https://frereit.de/aes_gcm/) and is using scrypt key derivation algorithm correctly (https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback , see notes about choosing proper salt).

If considering change of session cookies format, what about just using Crypt::JWT library instead?

@kraih
Copy link
Member

kraih commented Oct 11, 2024

If considering change of session cookies format, what about just using Crypt::JWT library instead?

Good idea. Worth investigating.

@guest20
Copy link

guest20 commented Oct 11, 2024

I'm not sure if JWT gives us much more than an extra header section with:

  • "which crypto to use" field (a source of vulns of honoured) and
  • "type" which is ... "jwt"

@latk
Copy link

latk commented Oct 11, 2024

i might work on a port of the mojo.js encrypted session implementation to Mojolicious. That's probably the best solution for security.

Even if the session cookie format is changed, a secure method for generating secrets is needed, as provided by this PR.

Having strong keys so that the cryptographic methods can uphold their security guarantees may be more important than deciding whether to offer only Integrity (via the current use of a HMAC), or Integrity+Confidentiality (by switching to an authenticated encryption method like AES-GCM).

If porting the JavaScript code, it could be made significantly more efficient:

  static async encrypt(secret: string, value: string): Promise<string> {
    const key = await scrypt(secret, 'salt', 32);
    const iv = await randomBytes(12);
    const cipher = crypto.createCipheriv('aes-256-gcm', key as crypto.CipherKey, iv);
  1. I am not convinced that this use of scrypt() has any security benefits over a fast cryptographic hash function like SHA-2, especially if the secret is already strong.
  2. Due to using a constant salt, the scrypt() call could be pre-computed during application startup, instead of re-running this for each encrypt() and decrypt().

Mojo.js doesn't have benchmarks involving session cookies, but if it had some then caching (or removing) the scrypt() call might save on the order of 100ms per request.

what about just using Crypt::JWT library instead?

+1 on sticking close to standard formats like JWT, as this neatly sidesteps many pitfalls from doing low-level crypto manually. (Aside from the weakness that a JWT decoder might fail to limit which algorithms are accepted.)

That particular library has a hard dependency on CryptX, which provides a CryptX::PRNG::random_bytes() function which would remove the need to manually open /dev/urandom. This would also solve the problem of a secure fallback on Windows.

But pulling in a large and security-critical XS dependency just for that might not be worth it? The current (and much simpler) approach to session cookies can already be secure (if strong keys are used).

@kraih
Copy link
Member

kraih commented Oct 11, 2024

Even if the session cookie format is changed, a secure method for generating secrets is needed, as provided by this PR.

Is it really? Why can't we outsource that problem to plugins on CPAN? Any core mojo APIs missing?

@stigtsp
Copy link
Author

stigtsp commented Oct 11, 2024

Even if the session cookie format is changed, a secure method for generating secrets is needed, as provided by this PR.

Is it really? Why can't we outsource that problem to plugins on CPAN? Any core mojo APIs missing?

@kraih Yes. Strong keys are required for HMAC signed cookies. I have no strong opinion on what CPAN modules or syscalls are used for randomness, as long as bits are from a csprng.

For example, NIST requires HMAC keys to be a minimum of 128 bits from a cryptographic random number generator: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-224.ipd.pdf

Problems:

  1. A well known string is used as default HMAC key. Even though its documented, and a warning is printed, it provides no security and could be a vulnerability if it ends up in production. CWE-321

  2. No key length requirement for users when setting keys. This leads to weak keys in the wild vulnerable to dictionary or brute force attacks, as described in Baking Mojolicious cookies by Antoine Cervoise. CWE-521

  3. mojo generate app generates a weak secret using sha1_sum $$ . steady_time . rand, which is not from a cryptographic number generator. CWE-338

This PR aims to start fixing these problems, so let me know if there is anything more I can do to help.

@kraih
Copy link
Member

kraih commented Oct 11, 2024

@kraih Yes. Strong keys are required for HMAC signed cookies. I have no strong opinion on what CPAN modules or syscalls are used for randomness, as long as bits are from a csprng.

Perhaps there was a miscommunication then. We make no claim to be secure by default, that's why we ask users to change their secrets and suggest to rotate regularly. The secrets we generate by default are strictly for development purposes, just like the TLS certificate we ship. It is the users responsibility to change both for production.

The default secrets being better with an optional CryptX dependency because of an alternative session implementation would merely be a positive side effect, not an end goal.

@jkramarz
Copy link

Out of curiosity, I checked how some top starred projects on Github with direct Mojolicious dependency are dealing with this problem:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants