nodejs / nodejs/node

tls.connect() ignores the `session` option when `socket` is provided (regression v22.22.3 → v22.23.1)

Ouverte
#64,402 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

net tls
Langage dominant
JavaScript
Étoiles
122k
Forks
37.3k
Merge moyen
4 j 2 h
PR mergées (30 j)
283

Description

Version

v22.23.1

Platform
Linux (docker node:22.23.1-bookworm-slim, x86_64); also reproduced on the non-slim image
Subsystem

tls

What steps will reproduce the bug?

Run the self-contained script below (starts a local TLS 1.2 server, then connects twice offering the first connection's session):

// Repro: tls.connect() ignores the `session` option when `socket` is provided.
// Resuming a TLS 1.2 session works with a direct tls.connect({host, port,
// session}) but silently does a full handshake when upgrading an existing
// net.Socket via tls.connect({socket, session}). Regressed between v22.22.3
// (wrapped path resumes) and v22.23.1 (wrapped path ignores the session).
//
// Real-world impact: FTPS servers with vsftpd require_ssl_reuse (e.g. Bambu
// Lab H2D 3D printers) reject every data connection from clients like
// basic-ftp, which upgrade the already-connected data socket and pass
// `session` exactly this way.
'use strict'
const tls = require('node:tls')
const net = require('node:net')

// Throwaway self-signed localhost cert so the repro is a single file.
const key = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDsN1UvQ0kux/m8
snGI8rPBL5kXYXe2E1IV/V8ImQDbFouay2bZIL4vPdhVsNGH7+oprUtQ9sgpKNue
GN96aGcIsUSZN6HcvFZhS78dk6FTBDaSVbUFijp59WS1WdLmmkFYva7SLGJ5VE9A
2tB4DSPcw0ofqESV2YMhHfsc4RhXJ6FcumWPv8yosT3lJAdZjxoBuxGtysY89bff
6ZNBIFnS8tb1W+L2qAk3p7GEIklJUDemmt1HDwu4vA/820Y0UB+KFNLNT4S/wUKw
J7nWJfLQ1A9BiQiRsJgHqRykXYFB9BO0uuQw+SxXtMEqU5CtlnB8Jf2LOfLbhjBx
acwD7oqfAgMBAAECggEAE+l8LE5DOobP9giiynUPEw9km9RzB22sgz8HBk4DhPRr
E0LnKhF5BrlzZZHQr+FY/2dkzG/pIpMXpEWbfRGU8eYjjrjiU52quGvusdsSg9F8
mixQZKWo1UQK18C5JwhEzuq6dGwaZvjk72YioaQV7FOoCXAhS/J4e8+vrdkJuLrs
iT5VvaB+Lpw6Se3OszyWQ4ceFs9WZPLljlxTFFXAADVvgIdMryygotag7zHvbXes
q7Gw/XaIm/RME1I3b7wxYQLeHn1lVQwb5ZjaSZB/2UEa6/s9PCNuerxJMg52cXKA
BDX6txAOyNoQBXU2aHmw7hGWfE2zYvSR9LO4j0RyMQKBgQD2wSwqd/CvYjEAS49e
hjPYr0jbrXVPHxUZeJvliQq3J4ocH+ARDUankVVbJmgxa4WccmDbLGFQil2UXHad
PMTAwysL1Fgvq9NabPJjqsBHLw0UPupDysI99c0K9EvfgjsUVBvtHhx1OSPJRtoa
hLhQpoAO0ASMn7iE7NezuzcW0QKBgQD1ERPNx+7RsQ/NuvC6qgI85DAaVxtqi6oN
6A/ddsz7wh5A0y0Io6Yca7ax8LJ86cPtUqeRfkKXjAZMYN887gQIIwAnJ+I7r8sb
96QXnZwNpoGIhpApbl6UW0WESqAZ6QNrdBVjUdjyHPmD2sxYWtzmmAQ08KD9YmH1
gjjtKobGbwKBgFJel5SxpwmUuJDFqF1AZUw+7w5N7+vyj0OKbFgKECalr0fGKDDp
Ap3rONgNkRh2MQrRb52aSf3twmFFIF9Kqs9CFzuCrdF0BUCZP6yfkkHw5efNPLxW
kdLHG6Q9eppoybn2fcAAnjVPVq/Y0/OoPDLH8dWAARTEOH8+5J5dr30RAoGBAIrR
9anny3y1FAKyFpNmooXjrOv+0+Ty4I2oGvSUy2EBgSmvdh8itIH88iyx8CmaMgZJ
f+qG3yoBg7/YUByvp1EEop2LljfBSQi6qxVFjpSc8Vto/li50JGxchuSOkbzG6DO
zR7Jyl670wvB8WdLcT6ediOW+1sNU/hCoASYxme5AoGAGTx0YXAEPlyzvv3+AKXW
e3XdzG9K+JMnFVgXF0KaMfzsS8zdQsfH2hRENiFBwPlzd0GH/CnbXZZjYtf1zvBR
m73fmbSH3K2CF2pf1+D8P+j9NCyPvBfINX+2EBsTjoDj8NswMiqO+yyFLgIIm/Yh
SwMI12dwWQ9jqc1PN6/pxkM=
-----END PRIVATE KEY-----`
const cert = `-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUTSO0VACyi1UT4Hr3aytSSsWVuwAwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcxMDA1MTUxNVoXDTM2MDcw
NzA1MTUxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA7DdVL0NJLsf5vLJxiPKzwS+ZF2F3thNSFf1fCJkA2xaL
mstm2SC+Lz3YVbDRh+/qKa1LUPbIKSjbnhjfemhnCLFEmTeh3LxWYUu/HZOhUwQ2
klW1BYo6efVktVnS5ppBWL2u0ixieVRPQNrQeA0j3MNKH6hEldmDIR37HOEYVyeh
XLplj7/MqLE95SQHWY8aAbsRrcrGPPW33+mTQSBZ0vLW9Vvi9qgJN6exhCJJSVA3
pprdRw8LuLwP/NtGNFAfihTSzU+Ev8FCsCe51iXy0NQPQYkIkbCYB6kcpF2BQfQT
tLrkMPksV7TBKlOQrZZwfCX9izny24YwcWnMA+6KnwIDAQABo1MwUTAdBgNVHQ4E
FgQUCn1orlDMdSN/3DcjQ0vIJGEamMswHwYDVR0jBBgwFoAUCn1orlDMdSN/3Dcj
Q0vIJGEamMswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAp+a/
cjmmEP0kbSNX8DMQfhhOV+sJhIQaeRP/uFlUmOwJq/BjoZyWUqfU94j6/VLM3ssz
6vYduhgTmQsy3oC/a73YHB5SwmwiT2CbQQ99zyoj65ZXIAFgsrPEEe3g4ufSuP5t
IFtKM6+QspM4VBWlXiUpI/AzPr7L2d9sJGoKTDSgnL+0YQx4PDeAixXtABKa6C+r
+olQEjaqnucctM3Rvr25co2scxbm7broFGLiFF26JyRKZjoatROB26Yp19QGdQHl
L5+7VMHzOa4fc5plpfZ+pEcGDdWA0jGhBTnwami9N9FpMW2/D/2PYh2TGj7nJ5vx
KfOP+J+ltK+bfdET5Q==
-----END CERTIFICATE-----`

const server = tls.createServer({ key, cert, maxVersion: 'TLSv1.2' })
server.listen(0, '127.0.0.1', async () => {
  const port = server.address().port
  const connectFresh = (session) => new Promise((resolve, reject) => {
    const s = tls.connect({ host: '127.0.0.1', port, rejectUnauthorized: false, maxVersion: 'TLSv1.2', session }, () => resolve(s))
    s.once('error', reject)
  })
  const connectWrapped = (session) => new Promise((resolve, reject) => {
    const plain = net.connect({ host: '127.0.0.1', port }, () => {
      const s = tls.connect({ socket: plain, rejectUnauthorized: false, maxVersion: 'TLSv1.2', session }, () => resolve(s))
      s.once('error', reject)
    })
    plain.once('error', reject)
  })

  const first = await connectFresh()
  const session = first.getSession()
  first.destroy()

  const fresh = await connectFresh(session)
  console.log(`fresh   tls.connect({host, port, session}): isSessionReused = ${fresh.isSessionReused()}`)
  fresh.destroy()

  const wrapped = await connectWrapped(session)
  console.log(`wrapped tls.connect({socket, session}):     isSessionReused = ${wrapped.isSessionReused()}`)
  const ok = wrapped.isSessionReused()
  wrapped.destroy()

  console.log(ok ? 'OK' : 'BUG: session option ignored on the wrapped-socket path')
  server.close()
  process.exitCode = ok ? 0 : 1
})
How often does it reproduce? Is there a required condition?

Always on v22.23.1. The required condition is passing an already-connected net.Socket via the socket option together with session.

What is the expected behavior? Why is that the expected behavior?

Both connections should resume the offered session (isSessionReused() === true), as documented for the session option and as v22.22.3 behaves:

fresh   tls.connect({host, port, session}): isSessionReused = true
wrapped tls.connect({socket, session}):     isSessionReused = true
OK
What do you see instead?

On v22.23.1 the wrapped-socket path silently performs a full handshake — the session option is accepted but has no effect:

fresh   tls.connect({host, port, session}): isSessionReused = true
wrapped tls.connect({socket, session}):     isSessionReused = false
BUG: session option ignored on the wrapped-socket path

Bisected across release lines: v22.22.3 resumes on both paths; v22.23.1 only on the fresh path.

Additional information

Real-world impact: FTPS servers running vsftpd with require_ssl_reuse (notably Bambu Lab H2-series 3D printers) require every data connection to resume the control connection's TLS session, and reject the transfer otherwise (522 SSL connection failed: session reuse required). FTP clients such as basic-ftp implement this by upgrading the already-connected passive data socket with tls.connect({ socket, session: controlSocket.getSession() }) — exactly the path this regression breaks — so on v22.23.x every such transfer fails while the same code works on earlier releases. (Working around it in our application by using a fresh tls.connect for the data connection.)

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Aucun fichier du dépôt ni aucun test n’est nommé. Commencez au point d’entrée tls.connect() et exécutez la reproduction autonome avec les versions concernées et précédentes de Node.js, en vous concentrant sur le chemin socket-plus-session. C’est terminé lorsque la session fournie est reprise à cet endroit, avec une couverture de régression pour les connexions nouvelles et les connexions utilisant un socket existant.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
javascript, node.js
Domaine
networking, security
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Calme
Clarté
Plutôt claire
Accessibilité débutants
55/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.