Coordinated Disclosure Timeline

Summary

Weak random number generator is used to sign JSON Web Token (JWT).

Product

YApi

Tested Version

The latest commit to the date: 8e1f654.

Details

JWT signing

Function randStr is using a cryptographically insecure pseudo-random number generator Math.random to create a randomly looking string that later is used to sign and verify issued tokens:

exports.randStr = () => {
  return Math.random()
    .toString(36)
    .substr(2);
};

When a new user is created the randStr function is used to generate a passsalt. It is used as a salt to hash the password and as the secret to sign a JWT that authenticates the user:

  async reg(ctx) {
...
    let passsalt = yapi.commons.randStr();
    let data = {
      username: params.username,
      password: yapi.commons.generatePassword(params.password, passsalt), //加密
      email: params.email,
      passsalt: passsalt,
...
      this.setLoginCookie(user._id, user.passsalt);
...
  }
...
  setLoginCookie(uid, passsalt) {
    let token = jwt.sign({ uid: uid }, passsalt, { expiresIn: '7 days' });
    this.ctx.cookies.set('_yapi_token', token, {
      expires: yapi.commons.expireDate(7),
      httpOnly: true
    });
...
  }

The Math.random returns a floating-point number that is more than or equal to 0.0 and less than 1.0. The call to toString(36) formats the number as base36. For example, 0.19280841320093556 gets encoded as 0.6xvo3g36129. The first two characters are trimmed and the result is 6xvo3g36129. The generated secret is mostly 10-12 characters long and consists of numbers and lowercase Latin alphabet characters only. Since the trimmed part is always 0. the calculation is completely reversible.

The weakness of cryptographically insecure pseudo-random number generators is that given some number of observed values the internal state of the generator can be recreated that reveals the numbers generated in the past or allows calculation of the future outputs. The internal state of the current implementation of Math.random in Node.js (a modification of XorShift128+ algorithm) can be recreated from three observed consecutive values.

To get the values an attacker may automate the user creation process to get three new user tokens rapidly, then run a brute force attack on the JWT HMAC signatures. This still should not be feasible to do in a reasonable time on a single machine like:

- Hashcat version: 6.1.1
- Nvidia GPUs: 4 * Tesla V100

Hashmode: 16500 - JWT (JSON Web Token)

Speed.#1.........:  1368.5 MH/s (244.80ms) @ Accel:32 Loops:128 Thr:1024 Vec:1
Speed.#2.........:  1368.9 MH/s (244.65ms) @ Accel:32 Loops:128 Thr:1024 Vec:1
Speed.#3.........:  1368.2 MH/s (244.80ms) @ Accel:32 Loops:128 Thr:1024 Vec:1
Speed.#4.........:  1368.3 MH/s (244.74ms) @ Accel:32 Loops:128 Thr:1024 Vec:1
Speed.#*.........:  5473.9 MH/s

However a very rough estimation shows that by using cloud computing the attack could cost from 8 000$ to 24 000$ to break the tokens (Cracking three values versus one value has very little penalty as cracking machines are optimized for multiple hashes and cracking a single hash doesn’t fully utilize computer resources). Please notice that the token’s 7 days expiration time doesn’t put a limit on the attack as the target is the passsalt value used to sign the token.

Impact

After successfully brute forcing the three pseudo-random values the attacker may recreate the passsalt values that are used to sign tokens of other users. It may be argued if there is an incentive to spend this amount of resources, but GPUs get better all the time.

CVE

Credit

This issue was discovered and reported by GHSL team member @JarLob (Jaroslav Lobačevski).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2020-228 in any communication regarding this issue.