CVE-2017-16031: Use of Insufficiently Random Values (CWE-330)
value: Socket.io is a realtime application framework that provides communication via websockets. Because socket.io 0.9.6 and earlier depends on `Math.random()` to create socket IDs, the IDs are predictable. An attacker is able to guess the socket ID and gain access to socket.io servers, potentially obtaining sensitive information.
CWE description: The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.
CVSS v3 base score: 7.5
Published: 2018-06-04T19:29Z
Repository: https://github.com/socketio/socket.io
Language: JavaScript
Files changed: lib/manager.js, test/manager.test.js
Commit date: 2012-04-26 14:28:00 -0700
Commit message: Making ID generation securely random

Diff: diff --git a/lib/manager.js b/lib/manager.js
index c30abb3..1fbc565 100644
--- a/lib/manager.js
+++ b/lib/manager.js
@@ -9,10 +9,11 @@
  */
 
 var fs = require('fs')
   , url = require('url')
   , tty = require('tty')
+  , crypto = require('crypto')
   , util = require('./util')
   , store = require('./store')
   , client = require('socket.io-client')
   , transports = require('./transports')
   , Logger = require('./logger')
@@ -137,10 +138,12 @@ function Manager (server, options) {
   var self = this;
   this.sockets.on('connection', function (conn) {
     self.emit('connection', conn);
   });
 
+  this.sequenceNumber = Date.now() | 0;
+ 
   this.log.info('socket.io started');
 };
 
 Manager.prototype.__proto__ = EventEmitter.prototype
 
@@ -700,13 +703,16 @@ Manager.prototype.handleClient = function (data, req) {
  * Generates a session id.
  *
  * @api private
  */
 
-Manager.prototype.generateId = function () {
-  return Math.abs(Math.random() * Math.random() * Date.now() | 0).toString()
-    + Math.abs(Math.random() * Math.random() * Date.now() | 0).toString();
+Manager.prototype.generateId = function (data) {
+  var rand = new Buffer(15); // multiple of 3 for base64
+  this.sequenceNumber = (this.sequenceNumber + 1) | 0;
+  rand.writeInt32BE(this.sequenceNumber, 11);
+  crypto.randomBytes(12).copy(rand);
+  return rand.toString('base64').replace(/\//g, '_').replace(/\+/g, '-');
 };
 
 /**
  * Handles a handshake request.
  *
@@ -750,11 +756,11 @@ Manager.prototype.handleHandshake = function (data, req, res) {
 
   this.authorize(handshakeData, function (err, authorized, newData) {
     if (err) return error(err);
 
     if (authorized) {
-      var id = self.generateId()
+      var id = self.generateId(newData || handshakeData)
         , hs = [
               id
             , self.enabled('heartbeats') ? self.get('heartbeat timeout') || '' : ''
             , self.get('close timeout') || ''
             , self.transports(data).join(',')
@@ -870,13 +876,13 @@ Manager.prototype.handlePacket = function (sessid, packet) {
 
 Manager.prototype.authorize = function (data, fn) {
   if (this.get('authorization')) {
     var self = this;
 
-    this.get('authorization').call(this, data, function (err, authorized) {
+    this.get('authorization').call(this, data, function (err, authorized, newData) {
       self.log.debug('client ' + authorized ? 'authorized' : 'unauthorized');
-      fn(err, authorized);
+      fn(err, authorized, newData);
     });
   } else {
     this.log.debug('client authorized');
     fn(null, true);
   }
diff --git a/test/manager.test.js b/test/manager.test.js
index 5b6335c..039f976 100644
--- a/test/manager.test.js
+++ b/test/manager.test.js
@@ -218,10 +218,90 @@ module.exports = {
       io.server.close();
       done();
     });
   },
 
+  'test authorization gets handshake data': function (done) {
+    var port = ++ports
+      , io = sio.listen(port)
+      , cl = client(port);
+
+    io.configure(function () {
+      function auth (data, fn) {
+        data.query.should.have.foo;
+        data.query.foo.should.eql('bar');
+        fn(null, false);
+      };
+
+      io.set('authorization', auth);
+    });
+
+    cl.get('/socket.io/{protocol}/?foo=bar', function (res, data) {
+      res.statusCode.should.eql(403);
+      data.should.match(/handshake unauthorized/);
+
+      cl.end();
+      io.server.close();
+      done();
+    });
+  },
+
+  'test that authorization can view handshake data': function (done) {
+    var port = ++ports
+      , io = sio.listen(port)
+      , cl = client(port);
+
+    io.configure(function () {
+      function auth (data, fn) {
+        data.query.should.have.foo;
+        data.query.foo.should.eql('bar');
+        fn(null, true);
+      };
+
+      io.set('authorization', auth);
+    });
+
+    cl.get('/socket.io/{protocol}/?foo=bar', function (res, data) {
+      res.statusCode.should.eql(200);
+
+      cl.end();
+      io.server.close();
+      done();
+    });
+  },
+
+  'test that authorization can change handshake data': function (done) {
+    var port = ++ports
+      , io = sio.listen(port)
+      , cl = client(port);
+
+    io.configure(function () {
+      function auth (data, fn) {
+        var replacement = { baz: 'qu' };
+        for (i in data) {
+          if (data.hasOwnProperty(i)) {
+            replacement[i] = data[i];
+          }
+        }
+        fn(null, true, replacement);
+      };
+
+      io.set('authorization', auth);
+    });
+
+    cl.get('/socket.io/{protocol}/', function (res, data) {
+      var id = data.split(':', 2)[0];
+      res.statusCode.should.eql(200);
+      io.handshaken[id].should.have.baz;
+      io.handshaken[id].baz.should.eql('qu');
+
+      cl.end();
+      io.server.close();
+      done();
+    });
+  },
+
   'test a handshake error': function (done) {
     var port = ++ports
       , io = sio.listen(port)
       , cl = client(port);
 

Vulnerable code: Manager.prototype.generateId = function () {
  return Math.abs(Math.random() * Math.random() * Date.now() | 0).toString()
    + Math.abs(Math.random() * Math.random() * Date.now() | 0).toString();
      var id = self.generateId()
    this.get('authorization').call(this, data, function (err, authorized) {
      fn(err, authorized);
Fixed code:   , crypto = require('crypto')
  this.sequenceNumber = Date.now() | 0;
 
Manager.prototype.generateId = function (data) {
  var rand = new Buffer(15); // multiple of 3 for base64
  this.sequenceNumber = (this.sequenceNumber + 1) | 0;
  rand.writeInt32BE(this.sequenceNumber, 11);
  crypto.randomBytes(12).copy(rand);
  return rand.toString('base64').replace(/\//g, '_').replace(/\+/g, '-');
      var id = self.generateId(newData || handshakeData)
    this.get('authorization').call(this, data, function (err, authorized, newData) {
      fn(err, authorized, newData);
  'test authorization gets handshake data': function (done) {
    var port = ++ports
      , io = sio.listen(port)
      , cl = client(port);

    io.configure(function () {
      function auth (data, fn) {
        data.query.should.have.foo;
        data.query.foo.should.eql('bar');
        fn(null, false);
      };

      io.set('authorization', auth);
    });

    cl.get('/socket.io/{protocol}/?foo=bar', function (res, data) {
      res.statusCode.should.eql(403);
      data.should.match(/handshake unauthorized/);

      cl.end();
      io.server.close();
      done();
    });
  },

  'test that authorization can view handshake data': function (done) {
    var port = ++ports
      , io = sio.listen(port)
      , cl = client(port);

    io.configure(function () {
      function auth (data, fn) {
        data.query.should.have.foo;
        data.query.foo.should.eql('bar');
        fn(null, true);
      };

      io.set('authorization', auth);
    });

    cl.get('/socket.io/{protocol}/?foo=bar', function (res, data) {
      res.statusCode.should.eql(200);

      cl.end();
      io.server.close();
      done();
    });
  },

  'test that authorization can change handshake data': function (done) {
    var port = ++ports
      , io = sio.listen(port)
      , cl = client(port);

    io.configure(function () {
      function auth (data, fn) {
        var replacement = { baz: 'qu' };
        for (i in data) {
          if (data.hasOwnProperty(i)) {
            replacement[i] = data[i];
          }
        }
        fn(null, true, replacement);
      };

      io.set('authorization', auth);
    });

    cl.get('/socket.io/{protocol}/', function (res, data) {
      var id = data.split(':', 2)[0];
      res.statusCode.should.eql(200);
      io.handshaken[id].should.have.baz;
      io.handshaken[id].baz.should.eql('qu');

      cl.end();
      io.server.close();
      done();
    });
  },