Filters coming HTML request content for any character, character set, a word (slang, swearword or whatever) or a sentence and returns an Express.js middleware. The middleware examines URL and HTML body contents of the request (by using body-parser) and blocks the request and returns a message if there is a forbidden character. In this way, provides protection against NoSQL (like MongoDB) injection attacks for Node.js applications.
What about NoSQL? What are the risks?
https://www.owasp.org/index.php/Testing_for_NoSQL_injection
https://blog.websecurify.com/2014/08/hacking-nodejs-and-mongodb.html
https://blog.websecurify.com/2014/08/attacks-nodejs-and-mongodb-part-to.html
content-filter
does not depend on NoSQL. You can use with purpose of filtering for anything. Also you can filter only URL or body data. See the sample project in use, guide, examples and performance tests results.
There are some risks when developing a project using NoSQL databases like MongoDB. The first one about URL and the other one about the content of req.body
. (sent data by an allowed user)
URL risk
If a malicious user tries to hack your database it's not so hard for anybody. Although it's not the right way, assume you use /users/123
as URL to get information and show to a user whose id
is 123
. However someone tries /users/%7B%24ne%3Anull%7D
as URL to get user informations after logged on your system the URL means /users/{$ne:null}
and likely your server sends the all users from your user collection.
Content risk
Malicous users might embed unwanted expression into the req.body
object as the URL risk.
If you want to check query parameters when you querying the collection there is another beautiful and lightweight solution which is named as mongo-sanitize.
However I've wanted to use a middleware to filter the data at the beginning of my Node.js app. Without any special labor at every MongoDB operation. Therefore I wrote this easy tool.
Installation
npm install content-filter --save
Using with Express.js
Just add the following two lines to your code;
var filter = require('content-filter')
app.use(filter());
A sample usage in the code;
var express = require('express')
var bodyParser = require('body-parser')
var filter = require('content-filter') /* STEP-1 */
var app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended:true}))
app.use(filter()); /* STEP-2 and that's all */
By the above default using, content-filter checks the request URL for {
and $
characters and functions and objects of the html body data property names for $
character coming by GET
, POST
, PUT
and DELETE
methods.
For example, content-filter checks "/users", "_id", "$ne", "address", "street" and "province" values from the below request. "/users" is examined for {
and $
characters and it passes. The others are examined for $
character and return 403 status with an error message because of "$ne" expression and hereby content-filter provide a reliable security for MongoDB applications against the injection attacks.
PUT /users HTTP/1.1
Host: webaddress.com
Content-Type: application/json
{
"_id": { "$ne": "A Malicious Expression" },
"address": {
"street": "Raising Road St.",
"province": "New Hampshire",
}
}
To read a summary that expresses basic usage of this library click over here
There are several options are used for to configure the module.
methodList:
Use this option to select method which will have been filtered and to stop the checking any method. The module checks for GET, POST, PUT and DELETE methods as default.
Configuring the filter only for POST
, PUT
and DELETE
methods;
app.use(filter({methodList:['POST', 'PUT', 'DELETE']}))
typeList:
Use this option to set filter data structure types of Javascript. Content-filter able to check every data type (object, function, number, string, boolean and symbol) to filter. Because an application cannot make a decision whether an expression is an innocent or a malicious. But a developer can. Content-filter checks object
and function
types as default considering MongoDB security.
Setting to check only string
data types;
app.use(filter({typeList:['string']}))
Note: To filter form data object for a string 'object' parameter must be found in typeList array
app.use(filter({typeList:['object','string']}))
urlBlackList:
Use this option to configure URL black list elements and to stop the filtering the URL content. The module checks {
and $
as default considering MongoDB security. Also urlBlackList
scope contains req.query
object. At the same time GET method requests are evaluated by using urlBlackList.
Note: Ascii code must be used for non-english and specific characters like space. (%20
must be used instead of space - more information)
Removing url filtering;
app.use(filter({urlBlackList:[null]}))
Configuring the filter for several words;
app.use(filter({urlBlackList:['word1', 'word2']}))
urlMessage:
Use this option to change the default request blocking message to show to the user.
app.use(filter({urlMessage: 'A forbidden expression has been found in URL: '}))
bodyBlackList:
Use this option to configure body black list elements and to stop the filtering the body content. The module checks for $
as default considering MongoDB security.
Removing body filtering;
app.use(filter({bodyBlackList:[null]}))
Configuring the filter for only test
characters;
app.use(filter({bodyBlackList:['test']}))
bodyMessage:
Use this option to change the default request blocking message to show to the user.
app.use(filter({bodyMessage: 'A forbidden expression has been found in form data: '}))
appendFound:
Use this option to append found forbidden characters to the end of error message. Error message can be default or overridden urlMessage and bodyMessage.
app.use(filter({appendFound: true}))
caseSensitive:
Use this option to stop the case-sensitive when filtering. The option is true
as default.
Keep in mind that bodyBlackList:['MALICIOUS']
is not equal to bodyBlackList:['Malicious']
. To catch both of them use caseSensitive
like the following:
app.use(filter({bodyBlackList:['malicious'], caseSensitive:false}))
Note: if content-filter
is used for to secure NoSQL DB by only checking special characters like $
and {
don't set this parameter false because of thought of performance. Actually it takes trivial too very small time.
checkNames:
Use this option to include property names of the objects -that will have been checked- to filter. The option is true
as default.
To stop filtering key values of objects this option must be set as false like as following:
app.use(filter({checkNames:false}))
)
Assume there is a request body object like the following which comes from a user form to delete selected goods from shoppingCarts
collection by user _id
value from our MongoDB. If checkNames
option is set as false
content-filter checks "abcd"
and 10
values if typeList contains 'string' and 'number' values. When checkNames
option is true
, content-filter checks id
, $ne
, "abcd"
, count
and 10
values under the same conditions.
{
_id: { $ne: "abcd" },
count: 10
}
shoppingCarts.delete({ _id: req.body._id })
By the way, the above method is not a best-practice. Instaed that, Passport.js and req.user._id
object could be used.
dispatchToErrorHandler:
Use this option to dispatch the error to your error handler middleware when content-filter
catches forbidden characters.
Configuring the filter to dispatch errors to the error handler;
app.use(filter({dispatchToErrorHandler: true}))
By this option content-filter
gives an error object to the next(error)
method like the following object to handle in error handler.
{ status: 403, code: "FORBIDDEN_CONTENT", message: Message }
combining options:
app.use(filter({urlBlackList:['&&'], bodyBlackList:['$ne'], methodList:['POST', 'PUT', 'DELETE'], dispatchToErrorHandler: true}))
or
var filterOptions = {
urlBlackList:['&&'],
urlMessage: 'A forbidden expression has been found in URL: ',
bodyBlackList:['$ne'],
bodyMessage: 'A forbidden expression has been found in form data: ',
methodList:['POST', 'PUT', 'DELETE'],
dispatchToErrorHandler: true,
}
app.use(filter(filterConf))
Configuring the filter for $
, {
, &&
and ||
characters;
var blackList = ['$','{','&&','||']
var options = {
urlBlackList: blackList,
bodyBlackList: blackList
}
app.use(filter(options))
Note: Today to secure NoSQL databases, several important characters ($
,{
,&&
and ||
) should be filtered. They can be changed over time.
Filtering the form data object for a string slang word 'sh*t' :)
Note: Most of the time blocking the word on the client side may be more accurate solution when considering the server performance.
However if the message comes from an API content-filter
might be solved this issue
Configuring the content-filter
:
(Actually default values of typeList, bodyMessage and methodList are already proper and not needed to set them)
var options = {
typeList:['object','string'],
bodyBlackList:['sh*t'],
bodyMessage: 'A forbidden expression has been found in form data: ',
methodList:['POST', 'PUT', 'DELETE']
}
app.use(filter(options))
Assume that the request below comes to the server:
POST /users HTTP/1.1
Host: webaddress.com
Content-Type: application/json
{
"address": {
"street": "The Sh*t Road St."
}
}
HTML status of the server response would be 403
(forbidden). And the response test like the following:
A forbidden expression has been found in form data: sh*t
Althought it's not sensible I'll filter URL data for 'admin_id' word for the sake of example.
Configuring the content-filter
:
app.use(filter({urlBlackList:['admin_id']}))
Assume that the request below comes to the server(full URL https://webaddress.com/?query=admin_id
):
GET /?query=admin_id HTTP/1.1
Host: webaddresss.com
HTML status of the server response would be 403
(forbidden). And the response test like the following:
A forbidden expression has been found in URL: admin_id
process.hrtime() has been used for to calculate elapsed time for the tests.
Test environment: Intel 3 Ghz Dual-Core CPU and 4 GB RAM
Action: POST
Test1
Data: Consists of nested objects which have 5 objects depth of the total. There were 9 elements at level-1, 11 elements at level-2, 4 elements at level-3, 2 elements at level-4 and 2 elements at level-5 too. URL data length is not important.
Options: Content-filter default options were used.
Result: Completed at 0.486934th ms = 0.0000486934th sec ( 1 ms = 0.001 sec )
Test2
Data: Consists of nested objects which have 2 objects depth of the total. 11 elements at level-1 and 4 elements at level-2. Level-1 has two long fileds. The first one contain a picture data as base64 string and its length is 168,275. Other one contains a string its length 2,389.
Options: typeList
has been set as ["object", "function","string"]
{
typeList: ["object", "function","string"]
}
Result: Completed at 0.386673rd ms = 0.0000386673rd sec
Test3
Data: The same with Test2 data.
Options: typeList
has been set as ["object", "function","string"]
and bodyBlackList
has been set as ["+8+L"]
{
typeList: ["object", "function","string"],
bodyBlackList: ["+8+L"]
}
Result: Content-filter found the forbidden string which is at 83,225th column at 0.629969th ms = 0.0000629969th sec after the process started.
Conclusion
This is a configurable and reliable tool to filter data.
Decription of object levels
{
level1_a: 1,
level1_b: 2,
level1_c: {
level2_a: "One",
level2_b: {
level3_a: "The level is three",
}
}
}
Credit: https://blog.tompawlak.org/measure-execution-time-nodejs-javascript