VYPR
Critical severityNVD Advisory· Published Aug 1, 2022· Updated Apr 22, 2025

mTLS client verification is skipped in fs2 on Node.js

CVE-2022-31183

Description

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. TLSSockets in server-mode. Client-mode TLSSockets are implemented via a different API. 3. mTLS as enabled via requestCert = true in TLSParameters. The default setting is false for server-mode TLSSockets. 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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
co.fs2:fs2-ioMaven
>= 3.1.0, < 3.2.113.2.11
co.fs2:fs2-io_2.12Maven
>= 3.1.0, < 3.2.113.2.11
co.fs2:fs2-io_3Maven
>= 3.1.0, < 3.2.113.2.11
co.fs2:fs2-io_2.13Maven
>= 3.1.0, < 3.2.113.2.11
co.fs2:fs2-io_sjs1_2.13Maven
>= 3.1.0, < 3.2.113.2.11
co.fs2:fs2-io_sjs1_3Maven
>= 3.1.0, < 3.2.113.2.11

Affected products

1

Patches

2
19ce392e8093

Merge pull request from GHSA-2cpx-6pqp-wf35

https://github.com/typelevel/fs2Christopher DavenportJul 26, 2022via ghsa
5 files changed · +94 11
  • io/js/src/main/scala/fs2/io/internal/facade/events.scala+3 0 modified
    @@ -38,6 +38,9 @@ private[io] trait EventEmitter extends js.Object {
       protected[io] def on[E, F](eventName: String, listener: js.Function2[E, F, Unit]): this.type =
         js.native
     
    +  protected[io] def once(eventName: String, listener: js.Function0[Unit]): this.type =
    +    js.native
    +
       protected[io] def once[E](eventName: String, listener: js.Function1[E, Unit]): this.type =
         js.native
     
    
  • io/js/src/main/scala/fs2/io/internal/facade/tls.scala+7 0 modified
    @@ -189,6 +189,13 @@ package tls {
     
         def alpnProtocol: String | Boolean = js.native
     
    +    def ssl: SSL = js.native
    +
    +  }
    +
    +  @js.native
    +  private[io] trait SSL extends js.Object {
    +    def verifyError(): js.Error = js.native
       }
     
     }
    
  • io/js/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala+31 8 modified
    @@ -30,6 +30,8 @@ import cats.effect.std.Dispatcher
     import cats.syntax.all._
     import fs2.io.internal.facade
     
    +import scala.scalajs.js
    +
     private[tls] trait TLSContextPlatform[F[_]]
     
     private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type =>
    @@ -69,14 +71,35 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type =>
                       }
                     )
                   } else {
    -                val options = params.toTLSSocketOptions(dispatcher)
    -                options.secureContext = context
    -                options.enableTrace = logger != TLSLogger.Disabled
    -                options.isServer = true
    -                TLSSocket.forAsync(
    -                  socket,
    -                  sock => new facade.tls.TLSSocket(sock, options)
    -                )
    +                Resource.eval(F.deferred[Either[Throwable, Unit]]).flatMap { verifyError =>
    +                  TLSSocket
    +                    .forAsync(
    +                      socket,
    +                      sock => {
    +                        val options = params.toTLSSocketOptions(dispatcher)
    +                        options.secureContext = context
    +                        options.enableTrace = logger != TLSLogger.Disabled
    +                        options.isServer = true
    +                        val tlsSock = new facade.tls.TLSSocket(sock, options)
    +                        tlsSock.once(
    +                          "secure",
    +                          { () =>
    +                            val requestCert = options.requestCert.getOrElse(false)
    +                            val rejectUnauthorized = options.rejectUnauthorized.getOrElse(true)
    +                            val result =
    +                              if (requestCert && rejectUnauthorized)
    +                                Option(tlsSock.ssl.verifyError())
    +                                  .map(e => new JavaScriptSSLException(js.JavaScriptException(e)))
    +                                  .toLeft(())
    +                              else Either.unit
    +                            dispatcher.unsafeRunAndForget(verifyError.complete(result))
    +                          }
    +                        )
    +                        tlsSock
    +                      }
    +                    )
    +                    .evalTap(_ => verifyError.get.rethrow)
    +                }
                   }
                 }
                 .adaptError { case IOException(ex) => ex }
    
  • io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala+48 1 modified
    @@ -106,7 +106,7 @@ class TLSSocketSuite extends TLSSuite {
           val msg = Chunk.array(("Hello, world! " * 20000).getBytes)
     
           val setup = for {
    -        tlsContext <- Resource.eval(testTlsContext)
    +        tlsContext <- Resource.eval(testTlsContext(true))
             addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1"))
             (serverAddress, server) = addressAndConnections
             client <- Network[IO]
    @@ -180,5 +180,52 @@ class TLSSocketSuite extends TLSSuite {
             .intercept[SSLException]
         }
     
    +    test("mTLS client verification") { // 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]
    +    }
    +
       }
     }
    
  • io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala+5 2 modified
    @@ -32,7 +32,8 @@ import fs2.io.file.Path
     import scala.scalajs.js
     
     abstract class TLSSuite extends Fs2Suite {
    -  def testTlsContext: IO[TLSContext[IO]] = Files[IO]
    +
    +  def testTlsContext(privateKey: Boolean): IO[TLSContext[IO]] = Files[IO]
         .readAll(Path("io/shared/src/test/resources/keystore.json"))
         .through(text.utf8.decode)
         .compile
    @@ -43,7 +44,9 @@ abstract class TLSSuite extends Fs2Suite {
             SecureContext(
               ca = List(certKey.cert.asRight).some,
               cert = List(certKey.cert.asRight).some,
    -          key = List(SecureContext.Key(certKey.key.asRight, "password".some)).some
    +          key =
    +            if (privateKey) List(SecureContext.Key(certKey.key.asRight, "password".some)).some
    +            else None
             )
           )
         }
    
659824395826

First attempt at test for GHSA-2cpx-6pqp-wf35

https://github.com/typelevel/fs2Arman BilgeJul 22, 2022via ghsa
2 files changed · +53 3
  • io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala+48 1 modified
    @@ -106,7 +106,7 @@ class TLSSocketSuite extends TLSSuite {
           val msg = Chunk.array(("Hello, world! " * 20000).getBytes)
     
           val setup = for {
    -        tlsContext <- Resource.eval(testTlsContext)
    +        tlsContext <- Resource.eval(testTlsContext(true))
             addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1"))
             (serverAddress, server) = addressAndConnections
             client <- Network[IO]
    @@ -180,5 +180,52 @@ class TLSSocketSuite extends TLSSuite {
             .intercept[SSLException]
         }
     
    +    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]
    +    }
    +
       }
     }
    
  • io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala+5 2 modified
    @@ -32,7 +32,8 @@ import fs2.io.file.Path
     import scala.scalajs.js
     
     abstract class TLSSuite extends Fs2Suite {
    -  def testTlsContext: IO[TLSContext[IO]] = Files[IO]
    +
    +  def testTlsContext(privateKey: Boolean): IO[TLSContext[IO]] = Files[IO]
         .readAll(Path("io/shared/src/test/resources/keystore.json"))
         .through(text.utf8.decode)
         .compile
    @@ -43,7 +44,9 @@ abstract class TLSSuite extends Fs2Suite {
             SecureContext(
               ca = List(certKey.cert.asRight).some,
               cert = List(certKey.cert.asRight).some,
    -          key = List(SecureContext.Key(certKey.key.asRight, "password".some)).some
    +          key =
    +            if (privateKey) List(SecureContext.Key(certKey.key.asRight, "password".some)).some
    +            else None
             )
           )
         }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.