Skip to content

Commit 94571c1

Browse files
indutnytargos
authored andcommitted
tls: emit session after verifying certificate
Prior to this patch `session` event was emitted after `secure` event on TLSSocket, but before `secureConnect` event. This is problematic for `https.Agent` because it must cache session only after verifying the remote peer's certificate. Connecting to a server that presents an invalid certificate resulted in the session being cached after the handshake with the server and evicted right after a certifiate validation error and socket's destruction. A request initiated during this narrow window would pick the faulty session, send it to the malicious server and skip the verification of the server's certificate. Fixes: https://hackerone.com/reports/811502 CVE-ID: CVE-2020-8172 PR-URL: nodejs-private/node-private#200 Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
1 parent 55e4c72 commit 94571c1

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed

lib/_tls_wrap.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const kSNICallback = Symbol('snicallback');
9191
const kEnableTrace = Symbol('enableTrace');
9292
const kPskCallback = Symbol('pskcallback');
9393
const kPskIdentityHint = Symbol('pskidentityhint');
94+
const kPendingSession = Symbol('pendingSession');
95+
const kIsVerified = Symbol('verified');
9496

9597
const noop = () => {};
9698

@@ -274,7 +276,11 @@ function requestOCSPDone(socket) {
274276
function onnewsessionclient(sessionId, session) {
275277
debug('client emit session');
276278
const owner = this[owner_symbol];
277-
owner.emit('session', session);
279+
if (owner[kIsVerified]) {
280+
owner.emit('session', session);
281+
} else {
282+
owner[kPendingSession] = session;
283+
}
278284
}
279285

280286
function onnewsession(sessionId, session) {
@@ -467,6 +473,8 @@ function TLSSocket(socket, opts) {
467473
this.authorized = false;
468474
this.authorizationError = null;
469475
this[kRes] = null;
476+
this[kIsVerified] = false;
477+
this[kPendingSession] = null;
470478

471479
let wrap;
472480
if ((socket instanceof net.Socket && socket._handle) || !socket) {
@@ -631,6 +639,8 @@ TLSSocket.prototype._destroySSL = function _destroySSL() {
631639
this.ssl._secureContext.context = null;
632640
}
633641
this.ssl = null;
642+
this[kPendingSession] = null;
643+
this[kIsVerified] = false;
634644
};
635645

636646
// Constructor guts, arbitrarily factored out.
@@ -1516,6 +1526,12 @@ function onConnectSecure() {
15161526
this.emit('secureConnect');
15171527
}
15181528

1529+
this[kIsVerified] = true;
1530+
const session = this[kPendingSession];
1531+
this[kPendingSession] = null;
1532+
if (session)
1533+
this.emit('session', session);
1534+
15191535
this.removeListener('end', onConnectEnd);
15201536
}
15211537

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const https = require('https');
9+
const fixtures = require('../common/fixtures');
10+
11+
const options = {
12+
key: fixtures.readKey('agent1-key.pem'),
13+
14+
// NOTE: Certificate Common Name is 'agent1'
15+
cert: fixtures.readKey('agent1-cert.pem'),
16+
17+
// NOTE: TLS 1.3 creates new session ticket **after** handshake so
18+
// `getSession()` output will be different even if the session was reused
19+
// during the handshake.
20+
secureProtocol: 'TLSv1_2_method'
21+
};
22+
23+
const ca = [ fixtures.readKey('ca1-cert.pem') ];
24+
25+
const server = https.createServer(options, function(req, res) {
26+
res.end('ok');
27+
}).listen(0, common.mustCall(function() {
28+
const port = this.address().port;
29+
30+
const req = https.get({
31+
port,
32+
path: '/',
33+
ca,
34+
servername: 'nodejs.org',
35+
}, common.mustNotCall(() => {}));
36+
37+
req.on('error', common.mustCall((err) => {
38+
assert.strictEqual(
39+
err.message,
40+
'Hostname/IP does not match certificate\'s altnames: ' +
41+
'Host: nodejs.org. is not cert\'s CN: agent1');
42+
43+
const second = https.get({
44+
port,
45+
path: '/',
46+
ca,
47+
servername: 'nodejs.org',
48+
}, common.mustNotCall(() => {}));
49+
50+
second.on('error', common.mustCall((err) => {
51+
server.close();
52+
53+
assert.strictEqual(
54+
err.message,
55+
'Hostname/IP does not match certificate\'s altnames: ' +
56+
'Host: nodejs.org. is not cert\'s CN: agent1');
57+
}));
58+
}));
59+
}));
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
const fixtures = require('../common/fixtures');
6+
const assert = require('assert');
7+
const tls = require('tls');
8+
9+
const options = {
10+
key: fixtures.readKey('agent1-key.pem'),
11+
12+
// NOTE: Certificate Common Name is 'agent1'
13+
cert: fixtures.readKey('agent1-cert.pem'),
14+
15+
// NOTE: TLS 1.3 creates new session ticket **after** handshake so
16+
// `getSession()` output will be different even if the session was reused
17+
// during the handshake.
18+
secureProtocol: 'TLSv1_2_method'
19+
};
20+
21+
const server = tls.createServer(options, common.mustCall((socket) => {
22+
socket.end();
23+
})).listen(0, common.mustCall(() => {
24+
let connected = false;
25+
let session = null;
26+
27+
const client = tls.connect({
28+
rejectUnauthorized: false,
29+
port: server.address().port,
30+
}, common.mustCall(() => {
31+
assert(!connected);
32+
assert(!session);
33+
34+
connected = true;
35+
}));
36+
37+
client.on('session', common.mustCall((newSession) => {
38+
assert(connected);
39+
assert(!session);
40+
41+
session = newSession;
42+
43+
client.end();
44+
server.close();
45+
}));
46+
}));

0 commit comments

Comments
 (0)