diff --git a/README.md b/README.md index de43c85..2326dd6 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,33 @@ var app = express(); app.use(bodyParser.urlencoded({extended: true})); app.use(bodyParser.json()); + +// To remove data, use: app.use(mongoSanitize()); +// Or, to replace prohibited characters with _, use: +app.use(mongoSanitize({ + replaceWith: '_' +})) + ``` ## What? -This module removes any keys in objects that begin with a `$` sign from `req.body`, `req.query` or `req.params`. +This module searches for any keys in objects that begin with a `$` sign or contain a `.`, from `req.body`, `req.query` or `req.params`. It can then either: + +- completely remove these keys and associated data from the object, or +- replace the prohibited characters with another allowed character. + +The behaviour is governed by the passed option, `replaceWith`. Set this option to have the sanitizer replace the prohibited characters with the character passed in. + +See the spec file for more examples. ## Why? -Object keys starting with a `$` are _reserved_ for use by MongoDB as operators. Without this sanitization, malicious users could send an object containing a `$` operator, which could change the context of a database operation. Most notorious is the `$where` operator, which can execute arbitrary JavaScript on the database. +Object keys starting with a `$` or containing a `.` are _reserved_ for use by MongoDB as operators. Without this sanitization, malicious users could send an object containing a `$` operator, or including a `.`, which could change the context of a database operation. Most notorious is the `$where` operator, which can execute arbitrary JavaScript on the database. -The best way to prevent this is to sanitize the received data, and remove any offending keys. +The best way to prevent this is to sanitize the received data, and remove any offending keys, or replace the characters with a 'safe' one. ## Credits diff --git a/index.js b/index.js index aafae51..0eab9fd 100644 --- a/index.js +++ b/index.js @@ -1,29 +1,52 @@ 'use strict'; -var sanitize = function(val) { - if(Array.isArray(val)) { - val.forEach(sanitize); - - } else if(val instanceof Object) { - Object.keys(val).forEach(function(key) { - if (/^\$|\./.test(key)) { - delete val[key]; - } else { - sanitize(val[key]); - } - }); +var TEST_REGEX = /^\$|\./, + REPLACE_REGEX = /^\$|\./g; + +var sanitize = function(val, options) { + options = options || {}; + + var replaceWith = null; + if(!(TEST_REGEX.test(options.replaceWith))) { + replaceWith = options.replaceWith; } - return val; + var act = function(val) { + if(Array.isArray(val)) { + val.forEach(act); + + } else if(val instanceof Object) { + Object.keys(val).forEach(function(key) { + var v = val[key]; + var noRecurse = false; + + if(TEST_REGEX.test(key)) { + delete val[key]; + if(replaceWith) { + val[key.replace(REPLACE_REGEX, replaceWith)] = v; + } else { + noRecurse = true; + } + } + + if(!noRecurse) { + act(v); + } + + }); + } + + return val; + }; + + return act(val); }; var middleware = function(options) { - options = options || {}; - return function(req, res, next) { ['body', 'params', 'query'].forEach(function(k) { if(req[k]) { - req[k] = sanitize(req[k]); + req[k] = sanitize(req[k], options); } }); next(); diff --git a/test.js b/test.js index c431d4e..7235038 100644 --- a/test.js +++ b/test.js @@ -6,156 +6,387 @@ var request = require('supertest'), sanitize = require('./index.js'); describe('Express Mongo Sanitize', function() { - var app = express(); - app.use(bodyParser.urlencoded({extended: true})); - app.use(bodyParser.json()); - app.use(sanitize()); - - app.post('/body', function(req, res){ - res.status(200).json({ - body: req.body - }); - }); + describe('Remove Data', function() { + var app = express(); + app.use(bodyParser.urlencoded({extended: true})); + app.use(bodyParser.json()); + app.use(sanitize()); - app.get('/query', function(req, res){ - res.status(200).json({ - query: req.query + app.post('/body', function(req, res){ + res.status(200).json({ + body: req.body + }); }); - }); - describe('Top-level object', function() { - it('should sanitize the query string', function(done) { - request(app) - .get('/query?q=search&$where=malicious&dotted.data=some_data') - .set('Accept', 'application/json') - .expect(200, { - query: { - q: 'search' - } - }, done); + app.get('/query', function(req, res){ + res.status(200).json({ + query: req.query + }); }); - it('should sanitize a JSON body', function(done) { - request(app) - .post('/body') - .send({ - q: 'search', - is: true, - and: 1, - even: null, - stop: undefined, - $where: 'malicious', - 'dotted.data': 'some_data' - }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect(200, { - body: { + describe('Top-level object', function() { + it('should sanitize the query string', function(done) { + request(app) + .get('/query?q=search&$where=malicious&dotted.data=some_data') + .set('Accept', 'application/json') + .expect(200, { + query: { + q: 'search' + } + }, done); + }); + + it('should sanitize a JSON body', function(done) { + request(app) + .post('/body') + .send({ q: 'search', is: true, and: 1, - even: null - } - }, done); + even: null, + stop: undefined, + $where: 'malicious', + 'dotted.data': 'some_data' + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200, { + body: { + q: 'search', + is: true, + and: 1, + even: null + } + }, done); + }); + + it('should sanitize a form url-encoded body', function(done) { + request(app) + .post('/body') + .send('q=search&$where=malicious&dotted.data=some_data') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Accept', 'application/json') + .expect(200, { + body: { + q: 'search' + } + }, done); + }); }); - it('should sanitize a form url-encoded body', function(done) { - request(app) - .post('/body') - .send('q=search&$where=malicious&dotted.data=some_data') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('Accept', 'application/json') - .expect(200, { - body: { - q: 'search' - } - }, done); + describe('Nested Object', function() { + it('should sanitize a nested object in the query string', function(done) { + request(app) + .get('/query?username[$gt]=foo&username[dotted.data]=some_data') + .set('Accept', 'application/json') + .expect(200, { + query: { + username: {} + } + }, done); + }); + + it('should sanitize a nested object in a JSON body', function(done) { + request(app) + .post('/body') + .send({ + username: { + $gt: 'foo', + 'dotted.data': 'some_data' + } + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: {} + } + }, done); + }); + + it('should sanitize a nested object in a form url-encoded body', function(done) { + request(app) + .post('/body') + .send('username[$gt]=foo&username[dotted.data]=some_data') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: {} + } + }, done); + }); + }); + + describe('Nested Object inside an Array', function() { + it('should sanitize a nested object in the query string', function(done) { + request(app) + .get('/query?username[0][$gt]=foo&username[0][dotted.data]=some_data') + .set('Accept', 'application/json') + .expect(200, { + query: { + username: [{}] + } + }, done); + }); + + it('should sanitize a nested object in a JSON body', function(done) { + request(app) + .post('/body') + .send({ + username: [{ + $gt: 'foo', + 'dotted.data': 'some_data' + }] + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: [{}] + } + }, done); + }); + + it('should sanitize a nested object in a form url-encoded body', function(done) { + request(app) + .post('/body') + .send('username[0][$gt]=foo&username[0][dotted.data]=some_data') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: [{}] + } + }, done); + }); }); }); - describe('Nested Object', function() { - it('should sanitize a nested object in the query string', function(done) { - request(app) - .get('/query?username[$gt]=foo&username[dotted.data]=some_data') - .set('Accept', 'application/json') - .expect(200, { - query: { - username: {} - } - }, done); + describe('Preserve Data', function() { + var app = express(); + app.use(bodyParser.urlencoded({extended: true})); + app.use(bodyParser.json()); + app.use(sanitize({ + replaceWith: '_' + })); + + app.post('/body', function(req, res){ + res.status(200).json({ + body: req.body + }); }); - it('should sanitize a nested object in a JSON body', function(done) { - request(app) - .post('/body') - .send({ - username: { - $gt: 'foo', + app.get('/query', function(req, res){ + res.status(200).json({ + query: req.query + }); + }); + + describe('Top-level object', function() { + it('should sanitize the query string', function(done) { + request(app) + .get('/query?q=search&$where=malicious&dotted.data=some_data') + .set('Accept', 'application/json') + .expect(200, { + query: { + q: 'search', + _where: 'malicious', + dotted_data: 'some_data' + } + }, done); + }); + + it('should sanitize a JSON body', function(done) { + request(app) + .post('/body') + .send({ + q: 'search', + is: true, + and: 1, + even: null, + stop: undefined, + $where: 'malicious', 'dotted.data': 'some_data' - } - }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect(200, { - body: { - username: {} - } - }, done); + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200, { + body: { + q: 'search', + is: true, + and: 1, + even: null, + _where: 'malicious', + dotted_data: 'some_data' + } + }, done); + }); + + it('should sanitize a form url-encoded body', function(done) { + request(app) + .post('/body') + .send('q=search&$where=malicious&dotted.data=some_data') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Accept', 'application/json') + .expect(200, { + body: { + q: 'search', + _where: 'malicious', + dotted_data: 'some_data' + } + }, done); + }); }); - it('should sanitize a nested object in a form url-encoded body', function(done) { - request(app) - .post('/body') - .send('username[$gt]=foo&username[dotted.data]=some_data') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('Accept', 'application/json') - .expect(200, { - body: { - username: {} - } - }, done); + describe('Nested Object', function() { + it('should sanitize a nested object in the query string', function(done) { + request(app) + .get('/query?username[$gt]=foo&username[dotted.data]=some_data') + .set('Accept', 'application/json') + .expect(200, { + query: { + username: { + _gt: 'foo', + dotted_data: 'some_data' + } + } + }, done); + }); + + it('should sanitize a nested object in a JSON body', function(done) { + request(app) + .post('/body') + .send({ + username: { + $gt: 'foo', + 'dotted.data': 'some_data' + } + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: { + _gt: 'foo', + dotted_data: 'some_data' + } + } + }, done); + }); + + it('should sanitize a nested object in a form url-encoded body', function(done) { + request(app) + .post('/body') + .send('username[$gt]=foo&username[dotted.data]=some_data') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: { + _gt: 'foo', + dotted_data: 'some_data' + } + } + }, done); + }); + }); + + describe('Nested Object inside an Array', function() { + it('should sanitize a nested object in the query string', function(done) { + request(app) + .get('/query?username[0][$gt]=foo&username[0][dotted.data]=some_data') + .set('Accept', 'application/json') + .expect(200, { + query: { + username: [{ + _gt: 'foo', + dotted_data: 'some_data' + }] + } + }, done); + }); + + it('should sanitize a nested object in a JSON body', function(done) { + request(app) + .post('/body') + .send({ + username: [{ + $gt: 'foo', + 'dotted.data': 'some_data' + }] + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: [{ + _gt: 'foo', + dotted_data: 'some_data' + }] + } + }, done); + }); + + it('should sanitize a nested object in a form url-encoded body', function(done) { + request(app) + .post('/body') + .send('username[0][$gt]=foo&username[0][dotted.data]=some_data') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Accept', 'application/json') + .expect(200, { + body: { + username: [{ + _gt: 'foo', + dotted_data: 'some_data' + }] + } + }, done); + }); }); }); - describe('Nested Object inside an Array', function() { - it('should sanitize a nested object in the query string', function(done) { - request(app) - .get('/query?username[0][$gt]=foo&username[0][dotted.data]=some_data') + describe('Preserve Data: prohibited characters', function() { + it('should not allow data to be replaced with a `$`', function(done) { + var app = express(); + app.use(bodyParser.urlencoded({extended: true})); + app.use(sanitize({ + replaceWith: '$' + })); + + app.get('/query', function(req, res){ + res.status(200).json({ + query: req.query + }); + }); + request(app) + .get('/query?q=search&$where=malicious&dotted.data=some_data') .set('Accept', 'application/json') .expect(200, { query: { - username: [{}] + q: 'search' } }, done); }); - it('should sanitize a nested object in a JSON body', function(done) { - request(app) - .post('/body') - .send({ - username: [{ - $gt: 'foo', - 'dotted.data': 'some_data' - }] - }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect(200, { - body: { - username: [{}] - } - }, done); - }); + it('should not allow data to be replaced with a `.`', function(done) { + var app = express(); + app.use(bodyParser.urlencoded({extended: true})); + app.use(sanitize({ + replaceWith: '.' + })); - it('should sanitize a nested object in a form url-encoded body', function(done) { - request(app) - .post('/body') - .send('username[0][$gt]=foo&username[0][dotted.data]=some_data') - .set('Content-Type', 'application/x-www-form-urlencoded') + app.get('/query', function(req, res){ + res.status(200).json({ + query: req.query + }); + }); + request(app) + .get('/query?q=search&$where=malicious&dotted.data=some_data') .set('Accept', 'application/json') .expect(200, { - body: { - username: [{}] + query: { + q: 'search' } }, done); });