Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Deprecate RC4 ciphers from the default ciphers list #25603

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 100 additions & 6 deletions doc/api/tls.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,88 @@ handshake extensions allowing you:
* SNI - to use one TLS server for multiple hostnames with different SSL
certificates.

## Modifying the Default Cipher Suite

Node.js is built with a default suite of enabled and disabled ciphers.
Currently, the default cipher suite is:

ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:HIGH:!RC4:!MD5:!aNULL:!EDH

This default can be overridden entirely using the `--cipher-list` command line
switch or `NODE_CIPHER_LIST` environment variable. For instance:

node --cipher-list=ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384

Setting the environment variable would have the same effect:

NODE_CIPHER_LIST=ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384

CAUTION: The default cipher suite has been carefully selected to reflect current
security best practices and risk mitigation. Changing the default cipher suite
can have a significant impact on the security of an application. The
`--cipher-list` and `NODE_CIPHER_LIST` options should only be used if
absolutely necessary.

### Using Legacy Default Cipher Suite ###

It is possible for the built-in default cipher suite to change from one release
of Node.js to another. For instance, v0.10.39 uses a different default than
v0.10.40. Such changes can cause issues with applications written to assume
certain specific defaults. To help buffer applications against such changes,
the `--enable-legacy-cipher-list` command line switch or `NODE_LEGACY_CIPHER_LIST`
environment variable can be set to specify a specific preset default:

# Use the v0.10.40 defaults
node --enable-legacy-cipher-list=v0.10.40
// or
NODE_LEGACY_CIPHER_LIST=v0.10.40

Currently, the values supported for the `enable-legacy-cipher-list` switch and
`NODE_LEGACY_CIPHER_LIST` environment variable include:

v0.10.40 - To enable the default cipher suite used in v0.10.40

ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH

These legacy cipher suites are also made available for use via the
`getLegacyCiphers()` method:

var tls = require('tls');
console.log(tls.getLegacyCiphers('v0.10.40'));

CAUTION: Changes to the default cipher suite are typically made in order to
strengthen the default security for applications running within Node.js.
Reverting back to the defaults used by older releases can weaken the security
of your applications. The legacy cipher suites should only be used if absolutely
necessary.

NOTE: Due to an error in Node.js v0.10.40, the default cipher list only applied
to servers using TLS. The default cipher list would _not_ be used by clients.
This behavior has been changed in v0.10.39 and the default cipher list is now
used by both the server and client when using TLS. However, when using
`--enable-legacy-cipher-list=v0.10.40`, Node.js is reverted back to the
v0.10.40 behavior of only using the default cipher list on the server.

### Cipher List Precedence

Note that the `--enable-legacy-cipher-list`, `NODE_LEGACY_CIPHER_LIST`,
`--cipher-list` and `NODE_CIPHER_LIST` options are mutually exclusive.

If the `NODE_CIPHER_LIST` and `NODE_LEGACY_CIPHER_LIST` environment variables
are both specified, the `NODE_LEGACY_CIPHER_LIST` setting will take precedence.

The `--cipher-list` and `--enable-legacy-cipher-list` command line options
will override the environment variables. If both happen to be specified, the
right-most (second one specified) will take precedence. For instance, in the
example:

node --cipher-list=ABC --enable-legacy-cipher-list=v0.10.40

The v0.10.40 default cipher list will be used.

node --enable-legacy-cipher-list=v0.10.40 --cipher-list=ABC

The custom cipher list will be used.

## tls.getCiphers()

Expand All @@ -120,6 +202,18 @@ Example:
console.log(ciphers); // ['AES128-SHA', 'AES256-SHA', ...]


## tls.getLegacyCiphers(version)

Returns a default cipher list used in a previous version of Node.js. The
version parameter must be a string whose value identifies previous Node.js
release version. The only value currently supported is `v0.10.40`.

A TypeError will be thrown if: (a) the `version` is any type other than a
string, (b) the `version` parameter is not specified, or (c) additional
parameters are passed in. An Error will be thrown if the `version` parameter is
passed in as a string but the value does not correlate to any known Node.js
release for which a default cipher list is available.

## tls.createServer(options, [secureConnectionListener])

Creates a new [tls.Server][]. The `connectionListener` argument is
Expand Down Expand Up @@ -151,13 +245,13 @@ automatically set as a listener for the [secureConnection][] event. The
conjunction with the `honorCipherOrder` option described below to
prioritize the non-CBC cipher.

Defaults to `AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH`.
Defaults to `ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:HIGH:!RC4:!MD5:!aNULL:!EDH`.
Consult the [OpenSSL cipher list format documentation] for details on the
format. ECDH (Elliptic Curve Diffie-Hellman) ciphers are not yet supported.


`AES128-GCM-SHA256` is used when node.js is linked against OpenSSL 1.0.1
or newer and the client speaks TLS 1.2, RC4 is used as a secure fallback.
or newer and the client speaks TLS 1.2.

**NOTE**: Previous revisions of this section suggested `AES256-SHA` as an
acceptable cipher. Unfortunately, `AES256-SHA` is a CBC cipher and therefore
Expand Down Expand Up @@ -333,7 +427,7 @@ Here is an example of a client of echo server as described previously:
// These are necessary only if using the client certificate authentication
key: fs.readFileSync('client-key.pem'),
cert: fs.readFileSync('client-cert.pem'),

// This is necessary only if the server uses the self-signed certificate
ca: [ fs.readFileSync('server-cert.pem') ]
};
Expand Down Expand Up @@ -525,7 +619,7 @@ A ClearTextStream is the `clear` member of a SecurePair object.

### Event: 'secureConnect'

This event is emitted after a new connection has been successfully handshaked.
This event is emitted after a new connection has been successfully handshaked.
The listener will be called no matter if the server's certificate was
authorized or not. It is up to the user to test `cleartextStream.authorized`
to see if the server certificate was signed by one of the specified CAs.
Expand All @@ -550,14 +644,14 @@ some properties corresponding to the field of the certificate.

Example:

{ subject:
{ subject:
{ C: 'UK',
ST: 'Acknack Ltd',
L: 'Rhys Jones',
O: 'node.js',
OU: 'Test TLS Certificate',
CN: 'localhost' },
issuer:
issuer:
{ C: 'UK',
ST: 'Acknack Ltd',
L: 'Rhys Jones',
Expand Down
14 changes: 12 additions & 2 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ function Credentials(secureProtocol, flags, context) {

exports.Credentials = Credentials;


exports.createCredentials = function(options, context) {
if (!options) options = {};

Expand All @@ -134,7 +133,18 @@ exports.createCredentials = function(options, context) {

if (options.cert) c.context.setCert(options.cert);

if (options.ciphers) c.context.setCiphers(options.ciphers);
if (options.ciphers) {
c.context.setCiphers(options.ciphers);
} else if (!(process._usingV1040Ciphers() && options.ciphers === undefined)) {
Copy link
Author

Choose a reason for hiding this comment

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

@jasnell Per #23947 (comment), we could replace this with:

} else if (!(process._usingV1040Ciphers()) {

since we said that null and undefinedwould mean "use the default ciphers list`. That would also mean that the empty string would make node use the empty string unless the v0.10.40 legacy cipher is used, but that also sounds reasonable.

// Set the ciphers to the default ciphers list unless
// --enable-legacy-cipher-list=v0.10.40 was passed on the command line and
// no ciphers value was passed explicitly. In that case, we want to
// preserve the previous buggy behavior that existed in v0.10.x until
// v0.10.39, otherwise, a lot of client code might be broken. Server
// side TLS/HTTPS code always sets a default cipher list explicitly so it
// never reaches this, even for versions < v0.10.39.
c.context.setCiphers(binding.DEFAULT_CIPHER_LIST);
}

if (options.ca) {
if (Array.isArray(options.ca)) {
Expand Down
21 changes: 17 additions & 4 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var _crypto = process.binding('crypto');

var crypto = require('crypto');
var util = require('util');
var net = require('net');
Expand All @@ -31,8 +33,9 @@ var constants = require('constants');

var Timer = process.binding('timer_wrap').Timer;

var DEFAULT_CIPHERS = 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' + // TLS 1.2
'RC4:HIGH:!MD5:!aNULL:!EDH'; // TLS 1.0
var DEFAULT_CIPHERS = _crypto.DEFAULT_CIPHER_LIST;

exports.getLegacyCiphers = _crypto.getLegacyCiphers;

// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations
// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more
Expand All @@ -44,7 +47,7 @@ exports.CLIENT_RENEG_WINDOW = 600;
exports.SLAB_BUFFER_SIZE = 10 * 1024 * 1024;

exports.getCiphers = function() {
var names = process.binding('crypto').getSSLCiphers();
var names = _crypto.getSSLCiphers();
// Drop all-caps names in favor of their lowercase aliases,
var ctx = {};
names.forEach(function(name) {
Expand All @@ -65,7 +68,7 @@ if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) {

var Connection = null;
try {
Connection = process.binding('crypto').Connection;
Connection = _crypto.Connection;
} catch (e) {
throw new Error('node.js not compiled with openssl crypto support.');
}
Expand Down Expand Up @@ -1335,6 +1338,16 @@ exports.connect = function(/* [port, host], options, cb */) {
var defaults = {
rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED
};
if (!process._usingV1040Ciphers()) {
Copy link
Author

Choose a reason for hiding this comment

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

@jasnell I don't think that this change is needed given that we have https://github.com/joyent/node/pull/25603/files#diff-437b078f0e3aa5b13790315b3813fd4fR138 too. Or am I missing something?

That would have the nice side effect of not needing to expose _usingV1040Ciphers publicly.

// only set the default ciphers if we are _not_ using the
// v0.10.40 legacy cipher list. Node v0.10.40 had a bug
// that failed to set the default ciphers on the default
// options. This has been fixed in v0.10.39 and above.
// However, when the user explicitly tells node to revert
// back to using the v0.10.40 cipher list, node should
// revert back to the original v0.10.40 behavior.
defaults.ciphers = DEFAULT_CIPHERS;
}
options = util._extend(defaults, options || {});

options.secureOptions = crypto._getSecureOptions(options.secureProtocol,
Expand Down
36 changes: 36 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2566,6 +2566,8 @@ static void PrintHelp() {
" --max-stack-size=val set max v8 stack size (bytes)\n"
" --enable-ssl2 enable ssl2\n"
" --enable-ssl3 enable ssl3\n"
" --cipher-list=val specify the default TLS cipher list\n"
" --enable-legacy-cipher-list=v0.10.40 \n"

Choose a reason for hiding this comment

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

Extra space before \n

"\n"
"Environment variables:\n"
#ifdef _WIN32
Expand All @@ -2577,6 +2579,8 @@ static void PrintHelp() {
"NODE_MODULE_CONTEXTS Set to 1 to load modules in their own\n"
" global contexts.\n"
"NODE_DISABLE_COLORS Set to 1 to disable colors in the REPL\n"
"NODE_CIPHER_LIST Override the default TLS cipher list\n"
"NODE_LEGACY_CIPHER_LIST=v0.10.40\n"

Choose a reason for hiding this comment

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

Shouldn't the description explain what this does?

"\n"
"Documentation can be found at http://nodejs.org/\n");
}
Expand Down Expand Up @@ -2652,6 +2656,18 @@ static void ParseArgs(int argc, char **argv) {
} else if (strcmp(arg, "--throw-deprecation") == 0) {
argv[i] = const_cast<char*>("");
throw_deprecation = true;
} else if (strncmp(arg, "--cipher-list=", 14) == 0) {
DEFAULT_CIPHER_LIST = arg + 14;
argv[i] = const_cast<char*>("");
} else if (strncmp(arg, "--enable-legacy-cipher-list=", 28) == 0) {
const char * legacy_list = crypto::LegacyCipherList(arg+28);
if (legacy_list != NULL) {
DEFAULT_CIPHER_LIST = legacy_list;
} else {
fprintf(stderr, "Error: An unknown legacy cipher list was specified\n");
exit(9);
}
argv[i] = const_cast<char*>("");
} else if (argv[i][0] != '-') {
break;
}
Expand Down Expand Up @@ -2928,6 +2944,26 @@ char** Init(int argc, char *argv[]) {
// Make inherited handles noninheritable.
uv_disable_stdio_inheritance();

// set the cipher list from the environment variable first,
// the command line switch will override if specified.
const char * cipher_list = getenv("NODE_CIPHER_LIST");
if (cipher_list != NULL) {
DEFAULT_CIPHER_LIST = cipher_list;
}

// Setting NODE_LEGACY_CIPHER_LIST will override the NODE_CIPHER_LIST
const char * leg_cipher_id = getenv("NODE_LEGACY_CIPHER_LIST");
if (leg_cipher_id != NULL) {
const char * leg_cipher_list =
crypto::LegacyCipherList(leg_cipher_id);
if (leg_cipher_list != NULL) {
DEFAULT_CIPHER_LIST = leg_cipher_list;
} else {
fprintf(stderr, "Error: An unknown legacy cipher list was specified\n");
exit(9);
}
}

// Parse a few arguments which are specific to Node.
node::ParseArgs(argc, argv);
// Parse the rest of the args (up to the 'option_end_index' (where '--' was
Expand Down
21 changes: 21 additions & 0 deletions src/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
startup.globalTimeouts();
startup.globalConsole();

startup.setupLegacyCiphers();

startup.processAssert();
startup.processConfig();
startup.processNextTick();
Expand Down Expand Up @@ -168,6 +170,25 @@
process._exiting = false;
};

startup.setupLegacyCiphers = function setupLegacyCiphers() {
process._usingV1040Ciphers = function _usingV1040Ciphers() {
// Returns true if the --enable-legacy-cipher-list command line
// switch, or the NODE_LEGACY_CIPHER_LIST environment variable
// are set to v0.10.40 and the DEFAULT_CIPHERS equal the v0.10.40
// list.
var crypto = process.binding('crypto');

var argv = process.execArgv;
if ((argv.indexOf('--enable-legacy-cipher-list=v0.10.40') > -1 ||
process.env.NODE_LEGACY_CIPHER_LIST === 'v0.10.40') &&
crypto.DEFAULT_CIPHER_LIST === crypto.getLegacyCiphers('v0.10.40')) {
return true;
}

return false;
};
};

startup.globalTimeouts = function() {
global.setTimeout = function() {
var t = NativeModule.require('timers');
Expand Down
Loading