Visitar URL original
tls: don't allow renegotiation or HRR to change SNI · nodejs/node@981d2de · GitHub
Skip to content

Commit 981d2de

Browse files
pimterryaduh95
authored andcommitted
tls: don't allow renegotiation or HRR to change SNI
Signed-off-by: Tim Perry <pimterry@gmail.com> Backport-PR-URL: nodejs-private/node-private#1044 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: nodejs-private/node-private#1030 CVE-ID: CVE-2026-123872
1 parent 799abe5 commit 981d2de

4 files changed

Lines changed: 99 additions & 43 deletions

File tree

‎lib/internal/tls/wrap.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1199,7 +1199,7 @@ TLSSocket.prototype._finishInit = function() {
11991199
return;
12001200

12011201
this.alpnProtocol = this._handle.getALPNNegotiatedProtocol();
1202-
// The servername could be set by TLSWrap::SelectSNIContextCallback().
1202+
// On a server, TLSWrap::PinServerName() sets it from the first ClientHello.
12031203
if (this.servername === null) {
12041204
this.servername = this._handle.getServername();
12051205
}

‎src/crypto/crypto_tls.cc‎

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ SSL_SESSION* GetSessionCallback(
104104
return w->ReleaseSession();
105105
}
106106

107+
// The server name is handled in EarlyClientHelloCallback, so this only
108+
// acknowledges it, which also has the TLS library store it in new sessions.
109+
int AcknowledgeServerName(SSL* s, int* ad, void* arg) {
110+
return SSL_TLSEXT_ERR_OK;
111+
}
112+
107113
// Reads a 16-bit length prefixed vector from the front of data. Returns
108114
// nothing if the length does not fit within data.
109115
std::optional<std::span<const uint8_t>> ReadVector16(
@@ -158,10 +164,7 @@ ssl_select_cert_result_t EarlyClientHelloCallback(const SSL_CLIENT_HELLO* ch) {
158164
// If we can't parse the hello SNI, reject it explicitly:
159165
if (!servername.has_value()) return ssl_select_cert_error;
160166

161-
// Session resumption must only be allowed for sessions with exactly the same
162-
// SNI. We use the SNI to derive the session id, so no handshake can resume
163-
// from a mismatched SNI value. Mismatches fall back to full handshakes:
164-
if (!w->BindSessionsToServerName(*servername)) return ssl_select_cert_error;
167+
if (!w->PinServerName(*servername)) return ssl_select_cert_error;
165168

166169
if (!w->should_suspend_for_client_hello()) return ssl_select_cert_success;
167170

@@ -191,10 +194,10 @@ int EarlyClientHelloCallback(SSL* s, int* al, void* arg) {
191194
return SSL_CLIENT_HELLO_ERROR;
192195
}
193196

194-
// Session resumption must only be allowed for sessions with exactly the same
195-
// SNI. We use the SNI to derive the session id, so no handshake can resume
196-
// from a mismatched SNI value. Mismatches fall back to full handshakes:
197-
if (!w->BindSessionsToServerName(*servername)) return SSL_CLIENT_HELLO_ERROR;
197+
if (!w->PinServerName(*servername)) {
198+
*al = SSL_AD_ILLEGAL_PARAMETER;
199+
return SSL_CLIENT_HELLO_ERROR;
200+
}
198201

199202
if (!w->should_suspend_for_client_hello()) return SSL_CLIENT_HELLO_SUCCESS;
200203

@@ -655,8 +658,7 @@ void TLSWrap::InitSSL() {
655658
// https://github.com/openssl/openssl/issues/7199#issuecomment-420670544
656659
SSL_set_info_callback(ssl_.get(), SSLInfoCallback);
657660

658-
if (is_server())
659-
sc_->SetSelectSNIContextCallback(SelectSNIContextCallback);
661+
if (is_server()) sc_->SetSelectSNIContextCallback(AcknowledgeServerName);
660662

661663
ConfigureSecureContext(sc_.get());
662664

@@ -1592,39 +1594,34 @@ void TLSWrap::SetServername(const FunctionCallbackInfo<Value>& args) {
15921594
SSL_set_tlsext_host_name(wrap->ssl_.get(), *servername);
15931595
}
15941596

1595-
int TLSWrap::SelectSNIContextCallback(SSL* s, int* ad, void* arg) {
1596-
TLSWrap* p = static_cast<TLSWrap*>(SSL_get_app_data(s));
1597-
Environment* env = p->env();
1598-
HandleScope handle_scope(env->isolate());
1599-
Context::Scope context_scope(env->context());
1600-
1601-
auto servername = SSLPointer::GetServerName(s);
1602-
if (!servername.has_value() ||
1603-
!Set(env, p->GetOwner(), env->servername_string(), servername.value()))
1604-
return SSL_TLSEXT_ERR_NOACK;
1605-
1606-
Local<Value> ctx = p->object()->Get(env->context(), env->sni_context_string())
1607-
.FromMaybe(Local<Value>());
1608-
1609-
// Acknowlege the name so it's correctly stored on the session:
1610-
if (ctx.IsEmpty() || !ctx->IsObject()) return SSL_TLSEXT_ERR_OK;
1611-
1612-
if (!env->secure_context_constructor_template()->HasInstance(ctx)) {
1613-
// Failure: incorrect SNI context object
1614-
Local<Value> err = Exception::TypeError(env->sni_context_err_string());
1615-
p->MakeCallback(env->onerror_string(), 1, &err);
1616-
return SSL_TLSEXT_ERR_NOACK;
1597+
// The connection's server name is fixed by its first ClientHello: later ones (a
1598+
// renegotiation, or the second of a HelloRetryRequest) must ask for exactly the
1599+
// same, as the connection's secure context was chosen for it.
1600+
bool TLSWrap::PinServerName(std::string_view servername) {
1601+
if (flags_.servername_pinned) {
1602+
if (servername == servername_) return true;
1603+
Debug(this,
1604+
"ClientHello asked for server name '%s', connection pinned to '%s'",
1605+
servername,
1606+
servername_);
1607+
return false;
16171608
}
1609+
flags_.servername_pinned = true;
1610+
servername_ = servername;
16181611

1619-
SecureContext* sc = Unwrap<SecureContext>(ctx.As<Object>());
1620-
CHECK_NOT_NULL(sc);
1621-
p->sni_context_ = BaseObjectPtr<SecureContext>(sc);
1622-
1623-
ConfigureSecureContext(sc);
1624-
CHECK_EQ(SSL_set_SSL_CTX(p->ssl_.get(), sc->ctx().get()), sc->ctx().get());
1625-
p->SetCACerts(sc);
1612+
if (!servername.empty()) {
1613+
HandleScope handle_scope(env()->isolate());
1614+
Context::Scope context_scope(env()->context());
1615+
if (!Set(env(), GetOwner(), env()->servername_string(), servername_)) {
1616+
return false;
1617+
}
1618+
}
16261619

1627-
return SSL_TLSEXT_ERR_OK;
1620+
// Session resumption must only be allowed for sessions with exactly the same
1621+
// SNI. We use the SNI to derive the session id context, so no handshake can
1622+
// resume from a mismatched SNI value. Mismatches fall back to full
1623+
// handshakes:
1624+
return BindSessionsToServerName(servername);
16281625
}
16291626

16301627
bool TLSWrap::BindSessionsToServerName(std::string_view servername) {
@@ -1803,6 +1800,7 @@ void TLSWrap::MemoryInfo(MemoryTracker* tracker) const {
18031800
tracker->TrackField("ocsp_response", ocsp_response_);
18041801
tracker->TrackField("sni_context", sni_context_);
18051802
tracker->TrackField("error", error_);
1803+
tracker->TrackField("servername", servername_);
18061804
if (pending_cleartext_input_)
18071805
tracker->TrackField("pending_cleartext_input", pending_cleartext_input_);
18081806
if (enc_in_ != nullptr)

‎src/crypto/crypto_tls.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class TLSWrap : public AsyncWrap,
9696
}
9797
inline void enable_session_callbacks() { flags_.session_callbacks = true; }
9898
bool BindSessionsToServerName(std::string_view servername);
99+
bool PinServerName(std::string_view servername);
99100
inline bool is_server() const { return kind_ == Kind::kServer; }
100101
inline bool is_client() const { return kind_ == Kind::kClient; }
101102
inline bool is_awaiting_new_session() const {
@@ -231,8 +232,6 @@ class TLSWrap : public AsyncWrap,
231232

232233
int SetCACerts(SecureContext* sc);
233234

234-
static int SelectSNIContextCallback(SSL* s, int* ad, void* arg);
235-
236235
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
237236
static void ClientHelloDone(const v8::FunctionCallbackInfo<v8::Value>& args);
238237
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -316,6 +315,7 @@ class TLSWrap : public AsyncWrap,
316315
v8::Global<v8::ArrayBufferView> ocsp_response_;
317316
BaseObjectPtr<SecureContext> sni_context_;
318317
BaseObjectPtr<SecureContext> sc_;
318+
std::string servername_;
319319

320320
// BIO buffers hold encrypted data.
321321
BIO* enc_in_ = nullptr; // StreamListener fills this for SSL_read().
@@ -353,6 +353,7 @@ class TLSWrap : public AsyncWrap,
353353
bool write_callback_scheduled : 1;
354354
bool has_active_write_issued_by_prev_listener : 1;
355355
bool alpn_callback_enabled : 1;
356+
bool servername_pinned : 1;
356357
};
357358
Flags flags_{};
358359

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Once a connection's secure context is chosen, later ClientHellos for the
4+
// same name - the second ClientHello of a TLS 1.3 HelloRetryRequest, or a TLS
5+
// 1.2 renegotiation - must leave it as it is. A context with no certificate of
6+
// its own (here one that only sets `ca`) keeps the server's certificate, and
7+
// must keep it for those too.
8+
9+
const common = require('../common');
10+
11+
if (!common.hasCrypto)
12+
common.skip('missing crypto');
13+
14+
const assert = require('node:assert');
15+
const tls = require('node:tls');
16+
const fixtures = require('../common/fixtures');
17+
const { isBoringSSL } = require('../common/crypto');
18+
19+
const server = tls.createServer({
20+
key: fixtures.readKey('agent2-key.pem'),
21+
cert: fixtures.readKey('agent2-cert.pem'),
22+
// Offering only this group makes the server answer a TLS 1.3 client whose
23+
// first key share is for another one with a HelloRetryRequest.
24+
ecdhCurve: 'P-521',
25+
}, common.mustCall((socket) => {
26+
assert.strictEqual(socket.servername, 'certless.example');
27+
socket.resume();
28+
}, isBoringSSL ? 1 : 2));
29+
server.addContext('certless.example', { ca: fixtures.readKey('ca1-cert.pem') });
30+
31+
function connect(maxVersion) {
32+
return new Promise((resolve) => {
33+
const socket = tls.connect({
34+
port: server.address().port,
35+
servername: 'certless.example',
36+
rejectUnauthorized: false,
37+
maxVersion,
38+
ecdhCurve: 'P-256:P-521',
39+
}, () => resolve(socket));
40+
});
41+
}
42+
43+
server.listen(0, common.mustCall(async () => {
44+
const retried = await connect('TLSv1.3');
45+
retried.end();
46+
47+
// BoringSSL servers don't support renegotiation.
48+
if (!isBoringSSL) {
49+
const renegotiating = await connect('TLSv1.2');
50+
await new Promise((resolve) => {
51+
renegotiating.renegotiate({}, common.mustSucceed(resolve));
52+
});
53+
renegotiating.end();
54+
}
55+
56+
server.close();
57+
}));

0 commit comments

Comments
 (0)