CVE-2022-31183 (Scala, published 2022-08-01T20:15Z)
Description: value: fs2 is a compositional, streaming I/O library for Scala. When establishing a server-mode `TLSSocket` using `fs2-io` on Node.js, the parameter `requestCert = true` is ignored, peer certificate verification is skipped, and the connection proceeds. The vulnerability is limited to: 1. `fs2-io` running on Node.js. The JVM TLS implementation is completely independent. 2. `TLSSocket`s in server-mode. Client-mode `TLSSocket`s are implemented via a different API. 3. mTLS as enabled via `requestCert = true` in `TLSParameters`. The default setting is `false` for server-mode `TLSSocket`s. It was introduced with the initial Node.js implementation of fs2-io in 3.1.0. A patch is released in v3.2.11. The requestCert = true parameter is respected and the peer certificate is verified. If verification fails, a SSLException is raised. If using an unpatched version on Node.js, do not use a server-mode TLSSocket with requestCert = true to establish a mTLS connection.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-295 \u2014 Improper Certificate Validation
The product does not validate, or incorrectly validates, a certificate.
Repository: https://github.com/typelevel/fs2
Files changed:
io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala
io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala
Commit message: First attempt at test for GHSA-2cpx-6pqp-wf35


Vulnerable code:
        tlsContext <- Resource.eval(testTlsContext)
  def testTlsContext: IO[TLSContext[IO]] = Files[IO]
          key = List(SecureContext.Key(certKey.key.asRight, "password".some)).some

Fixed code:
        tlsContext <- Resource.eval(testTlsContext(true))
    test("mTLS client verification".only) { // GHSA-2cpx-6pqp-wf35
      val msg = Chunk.array(("Hello, world! " * 20000).getBytes)

      val setup = for {
        serverContext <- Resource.eval(testTlsContext(true))
        clientContext <- Resource.eval(testTlsContext(false))
        addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1"))
        (serverAddress, server) = addressAndConnections
        client <- Network[IO]
          .client(serverAddress)
          .flatMap(
            clientContext
              .clientBuilder(_)
              .withParameters(
                TLSParameters(checkServerIdentity =
                  Some((sn, _) => Either.cond(sn == "localhost", (), new RuntimeException()))
                )
              )
              .build
          )
      } yield server.flatMap(s =>
        Stream.resource(
          serverContext
            .serverBuilder(s)
            .withParameters(TLSParameters(requestCert = true.some)) // mTLS
            .build
        )
      ) -> client

      Stream
        .resource(setup)
        .flatMap { case (server, clientSocket) =>
          val echoServer = server.map { socket =>
            socket.reads.chunks.foreach(socket.write(_))
          }.parJoinUnbounded

          val client =
            Stream.exec(clientSocket.write(msg)) ++
              clientSocket.reads.take(msg.size.toLong)

          client.concurrently(echoServer)
        }
        .compile
        .to(Chunk)
        .intercept[SSLException]
    }


  def testTlsContext(privateKey: Boolean): IO[TLSContext[IO]] = Files[IO]
          key =
            if (privateKey) List(SecureContext.Key(certKey.key.asRight, "password".some)).some
            else None