CVE-2017-16031 (published 2018-06-04T19:29Z)

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.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-330
cwe_name: Use of Insufficiently Random Values
description: The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.
extended_description: When product generates predictable values in a context requiring unpredictability, it may be possible for an attacker to guess the next value that will be generated, and use this guess to impersonate another user or access sensitive information.
         

Fix information:
repo_url: https://github.com/socketio/socket.io
commit_details: repo_url: https://github.com/socketio/socket.io
author: Martin Thomson
author_date: 2012-04-26 14:28:00-07:00
msg: Making ID generation securely random
repository: repo_url: https://github.com/socketio/socket.io
repo_name: socketio/socket.io
description: Realtime application framework (Node.JS server)
repo_language: TypeScript
owner: socketio
file_changes: filename: manager.js
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@
 var fs = require('fs')
   , url = require('url')
   , tty = require('tty')
+  , crypto = require('crypto')
   , util = require('./util')
   , store = require('./store')
   , client = require('socket.io-client')
@@ -139,6 +140,8 @@ function Manager (server, options) {
     self.emit('connection', conn);
   });
 
+  this.sequenceNumber = Date.now() | 0;
+ 
   this.log.info('socket.io started');
 };
 
@@ -702,9 +705,12 @@ Manager.prototype.handleClient = function (data, req) {
  * @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, '-');
 };
 
 /**
@@ -752,7 +758,7 @@ Manager.prototype.handleHandshake = function (data, req, res) {
     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') || '' : ''
@@ -872,9 +878,9 @@ 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');

programming_language: JavaScript
filename: manager.test.js
change_type: ModificationType.MODIFY
diff: @@ -220,6 +220,86 @@ module.exports = {
     });
   },
 
+  '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)

programming_language: JavaScript