express-mongo-sanitize
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -5,2 +5,7 @@ # Change Log | ||
## [1.2.0] - 2016-01-13 | ||
### Added | ||
- A new option `replaceWith` which can be used to replace offending characters in a key. This is an alternative to removing the data from the payload. | ||
## [1.1.0] - 2016-01-13 | ||
@@ -14,2 +19,3 @@ ### Added | ||
[1.2.0]: https://github.com/fiznool/express-mongo-sanitize/compare/v1.1.0...v1.2.0 | ||
[1.1.0]: https://github.com/fiznool/express-mongo-sanitize/compare/v1.0.0...v1.1.0 |
53
index.js
'use strict'; | ||
var sanitize = function(val) { | ||
if(Array.isArray(val)) { | ||
val.forEach(sanitize); | ||
var TEST_REGEX = /^\$|\./, | ||
REPLACE_REGEX = /^\$|\./g; | ||
} else if(val instanceof Object) { | ||
Object.keys(val).forEach(function(key) { | ||
if (/^\$|\./.test(key)) { | ||
delete val[key]; | ||
} else { | ||
sanitize(val[key]); | ||
} | ||
}); | ||
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); | ||
} | ||
@@ -28,0 +51,0 @@ }); |
{ | ||
"name": "express-mongo-sanitize", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Sanitize your express payload to prevent MongoDB operator injection.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -24,4 +24,11 @@ # Express Mongoose Sanitize | ||
app.use(bodyParser.json()); | ||
// To remove data, use: | ||
app.use(mongoSanitize()); | ||
// Or, to replace prohibited characters with _, use: | ||
app.use(mongoSanitize({ | ||
replaceWith: '_' | ||
})) | ||
``` | ||
@@ -31,9 +38,16 @@ | ||
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. | ||
@@ -40,0 +54,0 @@ ## Credits |
467
test.js
@@ -9,121 +9,361 @@ 'use strict'; | ||
describe('Express Mongo Sanitize', function() { | ||
var app = express(); | ||
app.use(bodyParser.urlencoded({extended: true})); | ||
app.use(bodyParser.json()); | ||
app.use(sanitize()); | ||
describe('Remove Data', 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 | ||
app.post('/body', function(req, res){ | ||
res.status(200).json({ | ||
body: req.body | ||
}); | ||
}); | ||
}); | ||
app.get('/query', function(req, res){ | ||
res.status(200).json({ | ||
query: req.query | ||
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' | ||
} | ||
}, done); | ||
}); | ||
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, | ||
stop: undefined, | ||
$where: 'malicious', | ||
'dotted.data': 'some_data' | ||
}) | ||
.set('Content-Type', 'application/json') | ||
.set('Accept', 'application/json') | ||
.expect(200, { | ||
body: { | ||
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' | ||
} | ||
@@ -133,29 +373,20 @@ }, 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' | ||
} | ||
@@ -162,0 +393,0 @@ }, done); |
17740
410
59