Repository: denoland/deno
Pull request: fix(ext/net): validate port in Deno.{connect,serve,listen} 

Closes https://github.com/denoland/deno/pull/21947
Closes https://github.com/denoland/deno/issues/21896

Changed files:
filename: ext/net/01_net.js
patch: @@ -33,9 +33,12 @@ const UDP_DGRAM_MAXSIZE = 65507;
 const {
   Error,
   Number,
+  NumberIsNaN,
+  NumberIsInteger,
   ObjectPrototypeIsPrototypeOf,
   ObjectDefineProperty,
   PromiseResolve,
+  RangeError,
   SafeSet,
   SetPrototypeAdd,
   SetPrototypeDelete,
@@ -531,10 +534,11 @@ const listenOptionApiName = Symbol("listenOptionApiName");
 function listen(args) {
   switch (args.transport ?? "tcp") {
     case "tcp": {
+      const port = validatePort(args.port);
       const { 0: rid, 1: addr } = op_net_listen_tcp(
         {
           hostname: args.hostname ?? "0.0.0.0",
-          port: Number(args.port),
+          port,
         },
         args.reusePort,
         args.loadBalanced ?? false,
@@ -558,14 +562,33 @@ function listen(args) {
   }
 }
 
+function validatePort(maybePort) {
+  if (typeof maybePort !== "number" && typeof maybePort !== "string") {
+    throw new TypeError(`Invalid port (expected number): ${maybePort}`);
+  }
+  if (maybePort === "") throw new TypeError("Invalid port: ''");
+  const port = Number(maybePort);
+  if (!NumberIsInteger(port)) {
+    if (NumberIsNaN(port) && !NumberIsNaN(maybePort)) {
+      throw new TypeError(`Invalid port: '${maybePort}'`);
+    } else {
+      throw new TypeError(`Invalid port: ${maybePort}`);
+    }
+  } else if (port < 0 || port > 65535) {
+    throw new RangeError(`Invalid port (out of range): ${maybePort}`);
+  }
+  return port;
+}
+
 function createListenDatagram(udpOpFn, unixOpFn) {
   return function listenDatagram(args) {
     switch (args.transport) {
       case "udp": {
+        const port = validatePort(args.port);
         const { 0: rid, 1: addr } = udpOpFn(
           {
             hostname: args.hostname ?? "127.0.0.1",
-            port: args.port,
+            port,
           },
           args.reuseAddress ?? false,
           args.loopback ?? false,
@@ -590,10 +613,11 @@ function createListenDatagram(udpOpFn, unixOpFn) {
 async function connect(args) {
   switch (args.transport ?? "tcp") {
     case "tcp": {
+      const port = validatePort(args.port);
       const { 0: rid, 1: localAddr, 2: remoteAddr } = await op_net_connect_tcp(
         {
           hostname: args.hostname ?? "127.0.0.1",
-          port: args.port,
+          port,
         },
       );
       localAddr.transport = "tcp";
@@ -626,4 +650,5 @@ export {
   shutdown,
   TcpConn,
   UnixConn,
+  validatePort,
 };

filename: ext/net/02_tls.js
patch: @@ -17,13 +17,12 @@ import {
   op_tls_start,
 } from "ext:core/ops";
 const {
-  Number,
   ObjectDefineProperty,
   TypeError,
   SymbolFor,
 } = primordials;
 
-import { Conn, Listener } from "ext:deno_net/01_net.js";
+import { Conn, Listener, validatePort } from "ext:deno_net/01_net.js";
 
 class TlsConn extends Conn {
   #rid = 0;
@@ -259,6 +258,7 @@ function listenTls({
   if (transport !== "tcp") {
     throw new TypeError(`Unsupported transport: '${transport}'`);
   }
+  port = validatePort(port);
 
   if (!hasTlsKeyPairOptions(arguments[0])) {
     throw new TypeError(
@@ -267,7 +267,7 @@ function listenTls({
   }
   const keyPair = loadTlsKeyPair("Deno.listenTls", arguments[0]);
   const { 0: rid, 1: localAddr } = op_net_listen_tls(
-    { hostname, port: Number(port) },
+    { hostname, port },
     { alpnProtocols, reusePort },
     keyPair,
   );

filename: tests/unit/serve_test.ts
patch: @@ -861,6 +861,88 @@ Deno.test(
   },
 );
 
+Deno.test({ permissions: { net: true } }, async function validPortString() {
+  const server = Deno.serve({
+    handler: (_request) => new Response(),
+    port: "4501" as unknown as number,
+  });
+  assertEquals(server.addr.transport, "tcp");
+  assertEquals(server.addr.port, 4501);
+  await server.shutdown();
+});
+
+Deno.test({ permissions: { net: true } }, function invalidPortFloat() {
+  assertThrows(
+    () =>
+      Deno.serve({
+        handler: (_request) => new Response(),
+        port: 45.1,
+      }),
+    TypeError,
+    `Invalid port: 45.1`,
+  );
+});
+
+Deno.test({ permissions: { net: true } }, function invalidPortNaN() {
+  assertThrows(
+    () =>
+      Deno.serve({
+        handler: (_request) => new Response(),
+        port: NaN,
+      }),
+    TypeError,
+    `Invalid port: NaN`,
+  );
+});
+
+Deno.test({ permissions: { net: true } }, function invalidPortString() {
+  assertThrows(
+    () =>
+      Deno.serve({
+        handler: (_request) => new Response(),
+        port: "some-non-number-string" as unknown as number,
+      }),
+    TypeError,
+    `Invalid port: 'some-non-number-string'`,
+  );
+});
+
+Deno.test({ permissions: { net: true } }, function invalidPortTooSmall() {
+  assertThrows(
+    () =>
+      Deno.serve({
+        handler: (_request) => new Response(),
+        port: -111,
+      }),
+    RangeError,
+    `Invalid port (out of range): -111`,
+  );
+});
+
+Deno.test({ permissions: { net: true } }, function invalidPortTooLarge() {
+  assertThrows(
+    () =>
+      Deno.serve({
+        handler: (_request) => new Response(),
+        port: 100000,
+      }),
+    RangeError,
+    `Invalid port (out of range): 100000`,
+  );
+});
+
+Deno.test({ permissions: { net: true } }, function invalidPortType() {
+  assertThrows(
+    () =>
+      Deno.serve({
+        handler: (_request) => new Response(),
+        port: true as unknown as number,
+      }),
+    TypeError,
+    `Invalid port (expected number): true`,
+  );
+});
+
 function createUrlTest(
   name: string,
   methodAndPath: string,