Harden Node.js WebSocket server handling
Client: nodejs

- Validate origin on WebSocket upgrade using the same CORS rules
  as HTTP requests
- Fix path containment check to use trailing separator
- Replace deprecated new Buffer() with Buffer.alloc()
- Sanitize interpolated header values in upgrade response

(04)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This closes #3391
diff --git a/lib/nodejs/lib/thrift/web_server.js b/lib/nodejs/lib/thrift/web_server.js
index f49f654..ad3a34c 100644
--- a/lib/nodejs/lib/thrift/web_server.js
+++ b/lib/nodejs/lib/thrift/web_server.js
@@ -27,6 +27,10 @@
 var MultiplexedProcessor =
   require("./multiplexed_processor").MultiplexedProcessor;
 
+function sanitizeHeader(value) {
+  return (value || "").replace(/[\r\n]/g, "");
+}
+
 var TBufferedTransport = require("./buffered_transport");
 var TBinaryProtocol = require("./binary_protocol");
 var InputBufferUnderrunError = require("./input_buffer_underrun_error");
@@ -84,7 +88,7 @@
    * @returns {Buffer} - The WebSocket frame, ready to send
    */
   encode: function (data, mask, binEncoding) {
-    var frame = new Buffer(wsFrame.frameSizeFromData(data, mask));
+    var frame = Buffer.alloc(wsFrame.frameSizeFromData(data, mask));
     //Byte 0 - FIN & OPCODE
     frame[0] =
       wsFrame.fin.FIN +
@@ -171,19 +175,19 @@
     }
     //MASK
     if (wsFrame.mask.TO_SERVER == (frame[1] & wsFrame.mask.TO_SERVER)) {
-      result.mask = new Buffer(4);
+      result.mask = Buffer.alloc(4);
       frame.copy(result.mask, 0, dataOffset, dataOffset + 4);
       dataOffset += 4;
     }
     //Payload
-    result.data = new Buffer(len);
+    result.data = Buffer.alloc(len);
     frame.copy(result.data, 0, dataOffset, dataOffset + len);
     if (result.mask) {
       wsFrame.applyMask(result.data, result.mask);
     }
     //Next Frame
     if (frame.length > dataOffset + len) {
-      result.nextFrame = new Buffer(frame.length - (dataOffset + len));
+      result.nextFrame = Buffer.alloc(frame.length - (dataOffset + len));
       frame.copy(result.nextFrame, 0, dataOffset + len, frame.length);
     }
     //Don't forward control frames
@@ -450,7 +454,8 @@
     var filename = path.resolve(path.join(baseDir, uri));
 
     //Ensure the basedir path is not able to be escaped
-    if (filename.indexOf(baseDir) != 0) {
+    var normalizedBase = baseDir.endsWith(path.sep) ? baseDir : baseDir + path.sep;
+    if (filename !== baseDir && filename.indexOf(normalizedBase) !== 0) {
       response.writeHead(400, "Invalid request path", {});
       response.end();
       return;
@@ -543,6 +548,14 @@
       }
     })
     .on("upgrade", function (request, socket, head) {
+      //Verify CORS origin for WebSocket upgrades
+      if (request.headers.origin && options.cors) {
+        if (!options.cors["*"] && !options.cors[request.headers.origin]) {
+          socket.write("HTTP/1.1 403 Origin not allowed\r\n\r\n");
+          socket.destroy();
+          return;
+        }
+      }
       //Lookup service
       var svc;
       try {
@@ -557,6 +570,9 @@
         request.headers["sec-websocket-key"] +
           "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
       );
+      var origin = sanitizeHeader(request.headers.origin);
+      var host = sanitizeHeader(request.headers.host);
+      var reqUrl = sanitizeHeader(request.url);
       socket.write(
         "HTTP/1.1 101 Switching Protocols\r\n" +
           "Upgrade: websocket\r\n" +
@@ -565,11 +581,11 @@
           hash.digest("base64") +
           "\r\n" +
           "Sec-WebSocket-Origin: " +
-          request.headers.origin +
+          origin +
           "\r\n" +
           "Sec-WebSocket-Location: ws://" +
-          request.headers.host +
-          request.url +
+          host +
+          reqUrl +
           "\r\n" +
           "\r\n",
       );
@@ -582,7 +598,7 @@
             //Prepend any existing decoded data
             if (data) {
               if (result.data) {
-                var newData = new Buffer(data.length + result.data.length);
+                var newData = Buffer.alloc(data.length + result.data.length);
                 data.copy(newData);
                 result.data.copy(newData, data.length);
                 result.data = newData;