Skip to content

Calling crypto.createCipheriv with a key of invalid length can cause errors in other consumers of the OpenSSL error queue #21281

Closed
@z0w0

Description

@z0w0
  • Version: v9.3.0 (initially discovered on v8.x but seems relevant to later versions too)
  • Platform: Windows 64-bit + ArchLinux 64-bit
  • Subsystem: Crypto

N.B. What follows is a naive speculation about what's going wrong since I'm not all too familiar with Node.js' internals.

When crypto.createCipheriv is called, it sets the key length using EVP_CIPHER_CTX_set_key_length. For some ciphers this will fail if it doesn't match a certain length. The crypto module throws a JS error if this is the case. If you look at the OpenSSL code for EVP_CIPHER_CTX_set_key_length, this error also goes into the OpenSSL error queue but is never removed by the Crypto module.

Because this error queue seems to be thread-global, you can get into situations where other stuff using OpenSSL (such as HTTPS/TLS) can think that the error was caused by its actions, not by a stale error being on the queue. Like so:

// This example shows how having a invalid key length call to createCipheriv
// caught and ignored while a TLS request is going on can cause the TLS request to fail,
// even though the error is completely separate from the TLS connection.
const crypto = require('crypto');
const https = require('https');

setInterval(() => {
  // Internally set_key_length is called but the error it produces (using EVPErr) isn't thrown away quick enough.
  try {
      crypto.createCipheriv('aes-256-ctr', 'bad key', crypto.randomBytes(16));
  } catch (err) {
    console.log(err.message);

    // Do nothing so that we continue with the old error on the OpenSSL error queue.
    // It would be there anyway, but in normal usage it would be more obvious why things are breaking because
    // you would never try/catch the createCipheriv. That's how this issue was discovered.
  }
}, 50);

const options = {
  hostname: 'google.com',
  port: 443,
  path: '/',
  method: 'GET'
};

const req = https.request(options, (res) => {
  console.log('statusCode:', res.statusCode);
  console.log('headers:', res.headers);

  res.on('data', (d) => {
    process.stdout.write(d);
  });
});

req.on('error', (err) => {
  // throws the EVPErr produces by createCipheriv.
  // At some point the Node.js TLS stack (possibly during TLS read)
  // throws the error off the OpenSSL error queue.
  throw err;
});

req.end();

Obviously this is a bad way to use createCipheriv, but it seems almost certainly wrong for the error to leak outside of the cipher code. Weirdly enough it seems like something else is pulling out the errors on its own, but if you create enough of them (hence the low ms setInterval) then it produces the error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cryptoIssues and PRs related to the crypto subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions