Skip to content

Commit

Permalink
feat: Adding new options (options.allowDots)
Browse files Browse the repository at this point in the history
Adding new option/feature, options.allowDots that is used for skipping the sanitization of data that has .(dot). This can be useful for nested document quering for mongoDb: https://docs.mongodb.com/manual/tutorial/query-embedded-documents/

Creating new tests that include the new option

Updating the documentation (README.md) file for the new option

Adressing issue: #36
  • Loading branch information
Blagoj5 committed Jan 23, 2021
1 parent 95cbfba commit aec9249
Show file tree
Hide file tree
Showing 4 changed files with 1,348 additions and 350 deletions.
62 changes: 52 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,49 @@ Express 4.x middleware which sanitizes user-supplied data to prevent MongoDB Ope
[![Dependency Status](https://david-dm.org/fiznool/express-mongo-sanitize.svg)](https://david-dm.org/fiznool/express-mongo-sanitize)
[![devDependency Status](https://david-dm.org/fiznool/express-mongo-sanitize/dev-status.svg)](https://david-dm.org/fiznool/express-mongo-sanitize#info=devDependencies)


## Installation

``` bash
```bash
npm install express-mongo-sanitize
```

## Usage

Add as a piece of express middleware, before defining your routes.

``` js
```js
const express = require('express');
const bodyParser = require('body-parser');
const mongoSanitize = require('express-mongo-sanitize');

const app = express();

app.use(bodyParser.urlencoded({extended: true}));
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: '_'
}))

app.use(
mongoSanitize({
replaceWith: '_',
})
);

// Or, to sanitize data that only contains $, without .(dot)
// Can be useful for letting data pass that is meant for querying nested documents. NOTE: This may cause some problems on older versions of MongoDb
// READ MORE: https://github.com/fiznool/express-mongo-sanitize/issues/36
app.use(
mongoSanitize({
allowDots: true,
})
);
```

You can also bypass the middleware and use the module directly:

``` js
```js
const mongoSanitize = require('express-mongo-sanitize');

const payload = {...};
Expand All @@ -54,10 +63,43 @@ mongoSanitize.sanitize(payload, {
replaceWith: '_'
});

// Exclude sanitization of . (dot), only sanitize data that contains $. This may cause some problems on older versions of mongoDb
mongoSanitize.sanitize(payload, {
allowDots: true
});

// Check if the payload has keys with prohibited characters
const hasProhibited = mongoSanitize.has(payload);
```

You can also combine allowDots with replaceWith options:

```js
const mongoSanitize = require('express-mongo-sanitize');

const payload = {...};

// This will only replace the $ with _(symbol) and allow dots
// Example. {'some.data': 'da', $where: 'bad'} -> {'some.data': 'da', _where: 'bad'}

// middleware
app.use(
mongoSanitize({
allowDots: true,
replaceWith: '_'
})
);

// module
mongoSanitize.sanitize(payload, {
allowDots: true,
replaceWith: '_'
});

// Check if the payload has keys with prohibited characters. Pass true as the second argument so it allows dots.
const hasProhibited = mongoSanitize.has(payload, true);
```

## What?

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:
Expand All @@ -71,7 +113,7 @@ See the spec file for more examples.

## Why?

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.
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, or replace the characters with a 'safe' one.

Expand Down
66 changes: 44 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
'use strict';

const TEST_REGEX = /^\$|\./;
const REPLACE_REGEX = /^\$|\./g;
let TEST_REGEX = /^\$|\./;
const TEST_REGEX_WITHOUT_DOT = /^\$/;
let REPLACE_REGEX = /^\$|\./g;

function isPlainObject(obj) {
return typeof obj === 'object' && obj !== null;
}

function withEach(target, cb) {
(function act(obj) {
if(Array.isArray(obj)) {
if (Array.isArray(obj)) {
obj.forEach(act);

} else if(isPlainObject(obj)) {
Object.keys(obj).forEach(function(key) {
} else if (isPlainObject(obj)) {
Object.keys(obj).forEach(function (key) {
const val = obj[key];
const resp = cb(obj, val, key);
if(resp.shouldRecurse) {
if (resp.shouldRecurse) {
act(obj[resp.key || key]);
}
});
}
})(target);

}

function has(target) {
function has(target, allowDots) {
let regex = TEST_REGEX;

if (allowDots) {
regex = TEST_REGEX_WITHOUT_DOT;
}

let hasProhibited = false;
withEach(target, function(obj, val, key) {
if(TEST_REGEX.test(key)) {
withEach(target, function (obj, val, key) {
if (TEST_REGEX.test(key)) {
hasProhibited = true;
return { shouldRecurse: false };
} else {
Expand All @@ -39,25 +44,38 @@ function has(target) {
return hasProhibited;
}

function sanitize(target, options) {
function sanitize(target, options, regex) {
options = options || {};

// Regex is not passed from the middleware
if (!regex) {
regex = TEST_REGEX;

if (options?.allowDots) {
TEST_REGEX = TEST_REGEX_WITHOUT_DOT;
}
}

let replaceWith = null;
if(!(TEST_REGEX.test(options.replaceWith))) {
if (!regex.test(options.replaceWith) && options.replaceWith !== '.') {
replaceWith = options.replaceWith;
}

withEach(target, function(obj, val, key) {
withEach(target, function (obj, val, key) {
let shouldRecurse = true;

if(TEST_REGEX.test(key)) {
if (regex.test(key)) {
delete obj[key];
if(replaceWith) {
if (replaceWith) {
key = key.replace(REPLACE_REGEX, replaceWith);
// Avoid to set __proto__ and constructor.prototype
// https://portswigger.net/daily-swig/prototype-pollution-the-dangerous-and-underrated-vulnerability-impacting-javascript-applications
// https://snyk.io/vuln/SNYK-JS-LODASH-73638
if (key !== "__proto__" && key !== "constructor" && key !== "prototype") {
if (
key !== '__proto__' &&
key !== 'constructor' &&
key !== 'prototype'
) {
obj[key] = val;
}
} else {
Expand All @@ -67,18 +85,22 @@ function sanitize(target, options) {

return {
shouldRecurse: shouldRecurse,
key: key
key: key,
};
});

return target;
}

function middleware(options) {
return function(req, res, next) {
['body', 'params', 'headers', 'query'].forEach(function(k) {
if(req[k]) {
req[k] = sanitize(req[k], options);
return function (req, res, next) {
['body', 'params', 'headers', 'query'].forEach(function (k) {
if (req[k]) {
req[k] = sanitize(
req[k],
options,
options?.allowDots ? TEST_REGEX_WITHOUT_DOT : TEST_REGEX
);
}
});
next();
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit aec9249

Please sign in to comment.