CVE-2019-1010263 (Other, published 2019-07-17T21:15Z)
Description: value: Perl Crypt::JWT prior to 0.023 is affected by: Incorrect Access Control. The impact is: allow attackers to bypass authentication by providing a token by crafting with hmac(). The component is: JWT.pm, line 614. The attack vector is: network connectivity. The fixed version is: after commit b98a59b42ded9f9e51b2560410106207c2152d6c.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-347 \u2014 Improper Verification of Cryptographic Signature
The product does not verify, or incorrectly verifies, the cryptographic signature for data.
Repository: https://github.com/DCIT/perl-Crypt-JWT
Files changed:
README
lib/Crypt/JWT.pm
t/compile.t
t/jws_no_key.t
Commit message: fix JWS signature validation (proper jwk header + kid_keys handling)


Vulnerable code:
        When the token header contains 'kid' item the corresponding key is
  $kid_keys = eval { decode_json($kid_keys) } unless ref $kid_keys;
  return undef unless ref $kid_keys eq 'HASH';
  #REMOVED: $header->{typ} = 'JWT' if !exists $header->{typ} && $args{auto_typ};
  my $key = defined $args{keypass} ? [$args{key}, $args{keypass}] : $args{key};
  if ($header->{kid} && $args{kid_keys}) {
    $key = $k if defined $k;
  croak "JWE: missing 'key'" if !$key;
  #REMOVED: $header->{typ} = 'JWT' if !exists $header->{typ} && $args{auto_typ};
    my $aa = $args{accepted_alg};
    if (ref($aa) eq 'Regexp') {
      croak "JWS: alg '$header->{alg}' does not match accepted_alg" if $header->{alg} !~ $aa;
    }
    elsif ($aa && (ref($aa) eq 'ARRAY' || !ref($aa))) {
      my %acca = ref $aa ? map { $_ => 1 } @$aa : ( $aa => 1 );
      croak "JWS: alg '$header->{alg}' not in accepted_alg" if !$acca{$header->{alg}};
    }
    # key
    my $key = defined $args{keypass} ? [$args{key}, $args{keypass}] : $args{key};
    my $kid = exists $header->{kid} ? $header->{kid} : $unprotected_header->{kid};
    if (!defined $key && defined $kid && $args{kid_keys}) {
      my $k = _kid_lookup($kid, $args{kid_keys}, $alg);
      $key = $k if defined $k;
    # if no key given, try to use 'jwk' value from header
    $key = $header->{jwk} if !$key && $header->{jwk};
    croak "JWS: missing 'key'" if !$key && $alg ne 'none';
    my $valid = _verify_jws($b64u_header, $b64u_payload, $b64u_sig, $alg, $key);
    croak "JWS: decode failed" if !$valid;
When the token header contains 'kid' item the corresponding key is looked up in C<kid_keys> list and used for token
use Test::More tests => 2;
my ( $header, $payload ) = decode_jwt( token => $jws, decode_header => 1 );

Fixed code:
        When the token header contains "kid" item the corresponding key is
        CHANGED in 0.23: When "kid_keys" is specified it croaks if token
        header does not contain "kid" value or if "kid" was not found in
        "kid_keys".

    key_from_jwk_header
        ADDED in 0.23

        1 - use "jwk" header value for validating JWS signature if neither
        "key" nor "kid_keys" specified, BEWARE: DANGEROUS, UNSECURE!!!

        0 (default) - ignore "jwk" header value when validating JWS
        signature

        Keep in mind that enabling "key_from_jwk_header" requires "jwk"
        header to exist and be an valid RSA/ECDSA public key (otherwise it
        croaks).

  return undef if !defined $kid || !defined $alg;
  $kid_keys = eval { decode_json($kid_keys) } if $kid_keys && !ref $kid_keys;
  croak "JWT: kid_keys must be a HASHREF or a valid JSON/HASH" if ref $kid_keys ne 'HASH';
  my $key;
  if (exists $args{key}) {
    $key = defined $args{keypass} ? [$args{key}, $args{keypass}] : $args{key};
  }
  elsif (exists $args{kid_keys}) {
    # BEWARE: stricter approach since 0.23
    # when 'kid_keys' specified it croaks if header doesn't contain 'kid' value or if 'kid' wasn't found in 'kid_keys'
    croak "JWE: kid_keys lookup failed" if !defined $k;
    $key = $k;
  croak "JWE: missing key" if !defined $key;
    croak "JWS: alg 'none' expects no signature" if $alg eq 'none' && defined $b64u_sig && length($b64u_sig) > 0;

    my $aa = $args{accepted_alg};
    if (ref $aa eq 'Regexp') {
      croak "JWS: alg '$alg' does not match accepted_alg" if $alg !~ $aa;
    }
    elsif (ref $aa eq 'ARRAY') {
      my %acca = map { $_ => 1 } @$aa;
      croak "JWS: alg '$alg' not in accepted_alg" if !$acca{$alg};
    }
    elsif (defined $aa) {
      croak "JWS: alg '$alg' not accepted_alg" if $aa ne $alg;
    }

    if ($alg ne 'none') {
      my $key;
      if (exists $args{key}) {
        $key = defined $args{keypass} ? [$args{key}, $args{keypass}] : $args{key};
      }
      elsif (exists $args{kid_keys}) {
        # BEWARE: stricter approach since 0.23
        # when 'kid_keys' specified it croaks if header doesn't contain 'kid' value or if 'kid' wasn't found in 'kid_keys'
        my $kid = exists $header->{kid} ? $header->{kid} : $unprotected_header->{kid};
        my $k = _kid_lookup($kid, $args{kid_keys}, $alg);
        croak "JWS: kid_keys lookup failed" if !defined $k;
        $key = $k;
      }
      elsif ($args{key_from_jwk_header}) {
        # BEWARE: stricter approach since 0.23
        # - header 'jwk' is by default ignored (unless given: key_from_jwk_header => 1)
        # - only RSA/ECDSA public keys are accepted
        my $k = $header->{jwk};
        croak "JWS: jwk header does not contain a key" if !defined $k || ref($k) ne 'HASH' || !defined $k->{kty};
        croak "JWS: jwk header allowed only for RSA/ECDSA" if $alg !~ /^(RS|PS|ES)/ || $k->{kty} !~ /^(RSA|EC)$/;
        croak "JWS: jwk header must be a public key" if $k->{d} || $k->{p} || $k->{q} || $k->{dp} || $k->{dq} || $k->{qi};
        $key = $k;
      }
      croak "JWS: missing key" if !defined $key;

      my $valid = _verify_jws($b64u_header, $b64u_payload, $b64u_sig, $alg, $key);
      croak "JWS: invalid signature" if !$valid;
When the token header contains C<kid> item the corresponding key is looked up in C<kid_keys> list and used for token
B<CHANGED in 0.23:> When C<kid_keys> is specified it croaks if token header does not contain C<kid> value or
if C<kid> was not found in C<kid_keys>.

=item key_from_jwk_header

B<ADDED in 0.23>

C<1> - use C<jwk> header value for validating JWS signature if neither C<key> nor C<kid_keys> specified, B<BEWARE: DANGEROUS, UNSECURE!!!>

C<0> (default) - ignore C<jwk> header value when validating JWS signature

Keep in mind that enabling C<key_from_jwk_header> requires C<jwk> header to exist and be an valid RSA/ECDSA public key (otherwise it croaks).

use Test::More tests => 3;

is($Crypt::KeyWrap::VERSION, $Crypt::JWT::VERSION, 'consistent version');
my ( $header, $payload ) = decode_jwt( token => $jws, decode_header => 1, key_from_jwk_header => 1 );