CVE-2022-31183 (published 2022-08-01T20:15Z)

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.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-295
cwe_name: Improper Certificate Validation
description: The product does not validate, or incorrectly validates, a certificate.
extended_description: When a certificate is invalid or malicious, it might allow an attacker to spoof a trusted entity by interfering in the communication path between the host and client. The product might connect to a malicious host while believing it is a trusted host, or the product might be deceived into accepting spoofed data that appears to originate from a trusted host.
         

Fix information:
repo_url: https://github.com/typelevel/fs2
commit_details: repo_url: https://github.com/typelevel/fs2
author: Arman Bilge
author_date: 2022-07-22 15:42:06-07:00
msg: First attempt at test for GHSA-2cpx-6pqp-wf35
repository: repo_url: https://github.com/typelevel/fs2
repo_name: typelevel/fs2
description: Compositional, streaming I/O library for Scala
repo_language: Scala
owner: typelevel
file_changes: filename: TLSSocketSuite.scala
change_type: ModificationType.MODIFY
diff: @@ -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]
+    }
+
   }
 }

programming_language: Scala
filename: TLSSuite.scala
change_type: ModificationType.MODIFY
diff: @@ -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
         )
       )
     }

programming_language: Scala