manniru
4/30/2014 - 3:28 AM

MongoDB Request Injection Attack in Node.js + Express Web Applications

MongoDB Request Injection Attack in Node.js + Express Web Applications

Overview
========
Students in my Web Programming class (G. Brown, S. Prassad, et al)
discovered that MongoDB request injection attacks also work on Node.js
+ Express web applications.  MongoDB request injection attacks have
been known for PHP web applications.

Impact
======
Attacker can view and download all the data in a MongoDB database
collection.

Affects
=======
Node.js + Express web applications

Additional Background Information and References
================================================
* http://docs.mongodb.org/manual/reference/operator/query/ne/
* http://www.acunetix.com/vulnerabilities/vulnerability/MongoDB_injection
* http://www.slideshare.net/wurbanski/nosql-no-security
* https://www.youtube.com/watch?v=lcO1BTNh8r8
* http://pastebin.com/JV15vA6K (FireEye vulnerabilities)

Example Vulnerable Node.js + Express Web Application
====================================================

File 1: package.json
====================
{
        "name": "2048-gamecenter",
        "version": "0.1.1",
        "dependencies": {
                "express": "latest",
                "mongodb": "latest",
                "body-parser": "latest"
        }
}

File 2: app.js
==============
// Express initialization
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser());
app.set('title', '2048 Game Center');

// Mongo initialization

var mongoUri = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL ||
'mongodb://localhost/2048';

var mongo = require('mongodb');
var db = mongo.Db.connect(mongoUri, function(error, databaseConnection) {
  db = databaseConnection;
});

app.get('/', function(request, response) {
  response.set('Content-Type', 'text/html');
  var indexPage = '';
  db.collection('scores', function(er, collection) {
    collection.find().sort({
      score: -1
    }).limit(100).toArray(function(err, cursor) {
      if (!err) {

        indexPage += "<!DOCTYPE HTML><html><head><title>2048 Game
        Center</title></head><body><h1>2048 Game
        Center</h1><table><tr><th>User</th><th>Score</th><th>Timestamp</th></tr>";

        for (var count = 0; count < cursor.length; count++) {

          indexPage += "<tr><td>" + cursor[count].username +
          "</td><td>" + cursor[count].score + "</td><td>" +
          cursor[count].created_at + "</td></tr>";

        }
        indexPage += "</table></body></html>"
        response.send(indexPage);
      } else {

        response.send('<!DOCTYPE
        HTML><html><head><title>ScoreCenter</title></head><body><h1>Whoops,
        something went terribly wrong!</h1></body></html>');

      }
    });
  });
});

// http://stackoverflow.com/questions/5710358/how-to-get-post-query-in-express-node-js
app.post('/submit.json', function(request, response) {
  // Enabling CORS
  // See http://stackoverflow.com/questions/11181546/node-js-express-cross-domain-scripting
  response.header("Access-Control-Allow-Origin", "*");
  response.header("Access-Control-Allow-Headers", "X-Requested-With");

  var username = request.body.username;
  var score = parseInt(request.body.score);
  var grid = request.body.grid;
  if (username != undefined && score != undefined && grid != undefined) {
    var toInsert = {
      "username": username,
      "score": score,
      "grid": grid,
      "created_at": Date()
    };
    db.collection('scores', function(er, collection) {
      var id = collection.insert(toInsert, function(err, saved) {
        if (err) {
          response.send(500)
        } else if (!saved) {
          response.send(500);
        } else {
          response.send(200);
        }
      });
    });
  }
  else {
    response.send("Data did not go through");
  }
});

app.get('/scores.json', function(request, response) {
  // Enabling CORS
  // See http://stackoverflow.com/questions/11181546/node-js-express-cross-domain-scripting
  response.header("Access-Control-Allow-Origin", "*");
  response.header("Access-Control-Allow-Headers", "X-Requested-With");

  // http://stackoverflow.com/questions/3390396/how-to-check-for-undefined-in-javascript
  var username = request.query.username;
  if (request.query.username === undefined) {
    response.send("[]");
  } else {
    db.collection('scores', function(er, collection) {
      collection.find({
        "username": username
      }).sort({
        score: -1
      }).limit(10).toArray(function(err, docs) {
        response.send(JSON.stringify(docs));
      });
    });
  }
});

app.listen(process.env.PORT || 5000);

To Run the Web Application Locally
===================================
Assume that Node.js and MongoDB are installed, and mongod is running

1. mkdir webapp;
2. Put app.js and package.json files into the folder "webapp"
3. cd webapp;
3. npm install; // Installs all required Node modules
4. node app.js; // Run web application

Add some data:
curl -d "username=mchow&score=10&grid={}" http://localhost:5000/submit.json;
curl -d "username=bobo&score=20&grid={}" http://localhost:5000/submit.json;
curl -d "username=poo&score=30&grid={}" http://localhost:5000/submit.json;

Proof-of-Concept
================
1. To return all data belonging to a specific username (e.g.,
mchow), go to: http://localhost:5000/scores.json?username=mchow (on a
web browser)

2. To return all the other data, go to
http://localhost:5000/scores.json?username[$ne]=mchow (on a web
browser).  Notice the [$ne] in the query, now an associative array
that will change the query to return all data where the username is
*not equal* to mchow!

Remediation
===========
Check and sanitize all GET and POST parameters.