Lucene search

K
hackeroneFwilhelmH1:811502
HistoryMar 05, 2020 - 5:30 p.m.

Node.js: Node.js: TLS session reuse can lead to hostname verification bypass

2020-03-0517:30:12
fwilhelm
hackerone.com
71

7.4 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

HIGH

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

NONE

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

5.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

NONE

AV:N/AC:M/Au:N/C:P/I:P/A:N

0.002 Low

EPSS

Percentile

60.5%

The Node.js TLS library supports client side reuse of TLS sessions when multiple connections to the same server are opened.

Code that wants to use this feature can listen for the โ€˜sessionโ€™ event (https://nodejs.org/api/tls.html#tls_event_session) on a tls.TLSSocket to get notified of newly created TLS sessions. The documentation for this event explicitly mentions that the passed sessions โ€œcan be used immediately or laterโ€.

The problem with this design is that โ€˜sessionโ€™ events are triggered even if verification of the server certificate hostname in onConnectSecure fails. (https://github.com/nodejs/node/blob/b1d4c13430c92e94920f0c8c9ba1295c075c9e89/lib/_tls_wrap.js#L1502):

onConnectSecure is triggered by the OpenSSL info callback (with the flag SSL_CB_HANDSHAKE_DONE) after a TLS handshake. The โ€˜sessionโ€™ event is triggered by OpenSSLs get_session_cb, which can happen before the info callback in TLS 1.2 and after in TLS 1.3 and which is triggered regardless of the result of onConnectSecure.

This means that sessions where the server presented an invalid certificate, or one with a wrong hostname, will trigger the session event and can end up being reused or stored in a cache.

That behavior is insecure, because resumed sessions will not be subjected to another hostname verification check as long as they are CA signed:

// Verify that serverโ€™s identity matches itโ€™s certificateโ€™s names
// Unless server has resumed our existing session
if (!verifyError && !this.isSessionReused()) {
const hostname = options.servername ||
options.host ||
(options.socket && options.socket._host) ||
โ€˜localhostโ€™;
const cert = this.getPeerCertificate(true);
verifyError = options.checkServerIdentity(hostname, cert);
}

In practice, this means that the immediate reuse described in the API documentation is always insecure and that session caches are at risk of storing insecure sessions. The most important implementation of a session cache is in the https library (https://github.com/nodejs/node/blob/b1d4c13430c92e94920f0c8c9ba1295c075c9e89/lib/https.js#L130): New sessions are stored in the cache when the โ€˜sessionโ€™ event is triggered and are evicted once a tls socket is closed with an error.

if (options._agentKey) {
// Cache new session for reuse
socket.on(โ€˜sessionโ€™, (session) => {
this._cacheSession(options._agentKey, session);
});

// Evict session on error
socket.once('close', (err) => {
  if (err)
    this._evictSession(options._agentKey);
});

}

This opens a small race window where an invalid session can be used by other HTTPs requests to the same host. The attached proof-of-concept wins the race reliably against a local server using a setImmediate() callback, but there are probably other ways this could be exploited in real world applications. I also did not fully investigate if there is a way to trigger the socket โ€˜closeโ€™ event with no error which would skip the session eviction and turn this into a 100% reliable bypass.

The POC requires a target server with a valid CA signed certificate (for an arbitrary hostname) and support for TLS resumption. Iโ€™ve attached a minimal golang https server that worked for me.

[fwilhelm@fwilhelm node]$ โ€ฆ/node/node-v13.9.0-linux-x64/bin/node poc.js
[!] First request failed:Host: nodejs.org. is not in the certโ€™s altnames: DNS:loca.host
[x] Starting second request
[x] Dumping globalAgent._sessionCache.map:
{
โ€˜nodejs.org:8444:::::::::::::::::TLSv1_2_method:โ€™: <Buffer 30 82 06 2f 02 01 01 02 02 03 04 04 02 13 01 04 20 cd b7 17 84 ac 9f 31 6f 1c cc 73 de 31 05 eb dc 60 62 df c7 c5 d5 8c b4 75 cc a7 28 1f d9 c0 22 04 โ€ฆ 1537 more bytes>
}
[!] Bypassed hostname verification. Server response: 200
{
date: โ€˜Thu, 05 Mar 2020 17:08:24 GMTโ€™,
โ€˜content-lengthโ€™: โ€˜29โ€™,
โ€˜content-typeโ€™: โ€˜text/plain; charset=utf-8โ€™,
connection: โ€˜closeโ€™
}

This bug is subject to a 90 day disclosure deadline. After 90 days elapse,
the bug report will become visible to the public. The scheduled disclosure
date is 2020-06-03. Disclosure at an earlier date is also possible if
agreed upon by all parties.

Impact

MitM of TLS connections

7.4 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

HIGH

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

NONE

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

5.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

NONE

AV:N/AC:M/Au:N/C:P/I:P/A:N

0.002 Low

EPSS

Percentile

60.5%