:muscle: Exhaustive checklist to assist in a security review of a Node.js web service code. Focused on Express and Hapi environments.
The next documents have been using as main references:
Related: :skull_and_crossbones: Awesome Node.js for penetration testers
Express: By default (if not in "production" mode) exposes the stack trace of the error to the client.
Hapi: Not exposed by default.
Express:
Create HTTP errors for Express, Koa, Connect, etc. with ease".
Hapi:
- So what's the best way of dealing with uncaught exceptions? There are many opinions floating around on this.
- You application shouldn't have uncaught exceptions. This is clearly insane.
- You should let your application crash, find uncaught exceptions and fix them. This is clearly insane.
- You should swallow errors silently. This is what lots of people do and it is bad.
- You should let your application crash, log errors and restart your process with - something like upstart, forever or monit. This is pragmatic.
- You should start using domains to handle errors. Clearly the way to go, although this is an experimental feature of Node.
Hapi: Poop: "hapi plugin for handling uncaught exceptions".
Heroku: Auto-restart through the Dyno crash restart policy.
Express/Hapi: See point 1.4
Express: Enabled by default, two options:
app.disable('x-powered-by').Hapi: Disabled by default.
Express: The middleware "body-parser" provides a comfortable mechanism to support this.
Hapi: Multiple options can be set for an specific route ("parse" option).
If parsing is enabled and the 'Content-Type' is known (for the whole payload as well as parts), the payload is converted into an object when possible
Express: Use Helmet middleware ("nocache" plugin). Remember to also disable the Etag ("noEtag").
Hapi: All kind of cache disabled by default, just confirm the code it's not using anyone included here. They can also be enabled for an specific route.
Express: Use Helmet middleware ("xssFilter" plugin) to improve protection in new browsers.
Hapi: Disabled by default, the framework supports it through the route options "security" ("xss" field).
Handlebars: The function "escapeExpression" is used by default.
Handlebars HTML-escapes values returned by a {{expression}}. If you don't want Handlebars to escape a value, use the "triple-stash", {{{"*).
Dust.js: Enabled by default.
All output values are escaped to avoid Cross Site Scripting (XSS) unless you use filters"*).
Swig: The options "autoescape" is needed.
Always use an validator for all the parameters used in each route:
Express: express-validator
Hapi: joi
However, contextual escaping is missing in most template frameworks including Handlebars JS, React JSX, and Dust JS.
(by Yahoo’s Paranoid Labs).
Their solutions: secure-handlebars, express-secure-handlebars.
Express: Use Helmet middleware ("ienoopen" plugin). Hapi: Disabled by default, the framework supports it through the route options "security" ("noOpen" field).
ESLint rule: "strict".
ESLint rules:
ESLint rule: "no-implied-eval".
ESLint rule: "detect-child-process".
ESLint rule: "detect-non-fs-filename".
ESLint rule: "detect-non-literal-require".
ESLint rule: "detect-non-literal-regexp".
Deep explanation and demo included in the section A8 of NodeGoat tutorial.
Express: csurf
Hapi: crumb
Express: Use Helmet middleware ("csp" plugin).
Hapi: blankie module.
Express: Use Helmet middleware ("frameguard" plugin).
Hapi: Disabled by default, the framework supports it through the route options "security" ("xframe" field).
Express: Use Helmet middleware ("nosniff" plugin).
Hapi: Disabled by default, the framework supports it through the route options "security" ("noSniff" field).
The core "path" module "extname" method is a simple option.
I want to restrict a path to something within a given root dir, I usually do something like this
(from createWriteStream vulnerable to path traversal?)
var safePath = path.join(safeRoot, path.join('/', unsafePath));
ie: email, Slack, etc. (CWE-778)
By importance ("warn", "info", etc.) and/or file ("server").
Use an independent logger or a debugger with a debug level as the default to print always. So we're using the term "logger" (or "logging") to refer to both from now.
"debug": Using it all logs will have the same structure and we can change what to see using the "DEBUG" environment variable. Moreover it's the same used in Express.
ie: When you run a worker or standalone script.
Use log rotation and increate the limits until you consider it's safe. BTW the free plan of the wide used cloud services (over 48 h.) is not enough.
Look for another input (ie: session, DB) if possible or sanitize them before. The reason is to avoid an attacker impersonation and/or track covering (CWE-117)
Log encryption (or part of them, ie: using a secure hash). Please refer to next section ("Cryptography").
Specially before the user authentication (ie: sending a password).
Express: express-force-ssl
Extremely simple middleware for requiring some or all pages to be visited over SSL.
Hapi: hapi-require-https
hapi http -> https redirection for servers behind a reverse proxy.
Heroku & SSL:
Express: Use Helmet middleware ("hsts" plugin).
Hapi: Disabled by default, the framework supports it through the route options "security" ("hsts" field).
A tool that can help to find them is GitRob.
Note that the upgrade to OpenSSL 1.0.1s in Node.js v0.12.11 removed internal SSLv2 support. The change in this release was originally intended for v0.12.11. The --enable-ssl2 command line argument now produces an error rather than being a no-op."
Confirm the server is using a correct value for these options when requesting through the TLS Node.js core module: "rejectUnauthorized" (default:true),"checkServerIdentity", "secureProtocol".
A good to verify possible problems is "sslyze".
A good option: Let's Encryt, an open CA. Automatic HTTPS Certificates for Node.js.
Automatic live renewal.
On-the-fly HTTPS certificates for Dynamic DNS (in-process, no server restart).
Works with node cluster out of the box.
Free SSL (HTTPS Certificates for TLS) 90-day certificates.
Express: letsencrypt-express.
Hapi and Hapi middleware: letsencrypt-hapi.
Implement it manually, no serious solution found out there to help with this.
Express middlewares:
Hapi:
The basic idea is to use a secure method (random and enough length) to generate each user session identifier. The best option, as always, is to use a mature option.
Express:
Hapi:
Express: The connect-redis library supports the "ttl" options to set the session expiration.
Hapi:
expiresIn - relative expiration expressed in the number of milliseconds since the item was saved in the cache. Cannot be used together with expiresAt.
expiresAt - time of day expressed in 24h notation using the 'HH:MM' format, at which point all cache records expire. Uses local time. Cannot be used together with expiresIn.
staleIn - number of milliseconds to mark an item stored in cache as stale and attempt to regenerate it when generateFunc is provided. Must be less than expiresIn.
staleTimeout - number of milliseconds to wait before checking if an item is stale.
Express: The "express-session" middleware includes the methods "Session.Destroy" and "store.destroy" (persistent) to manage it in a consistent way.
Hapi: Native server cache manages it correctly through the supported storages via catbox.
Express: The "express-session" middleware offers the method "regenerate" to make it easier.
Hapi: here we have the method "generateFunc". Check point 6.1 tips to know more.
Express: The "express-session" middleware offers the option "name".
Hapi: Cookies enabled by default, the option "name" in the method "server.state".
Express: The "express-session" middleware offers the option "secure".
Hapi: Cookies enabled by default, the option "isSecure" in the method "server.state".
Express: The "express-session" middleware offers the option "httpOnly".
Hapi: Cookies enabled by default, the option "isHttpOnly" in the method "server.state".
Express: The "express-session" middleware offers the option "secret".
Hapi: Cookies enabled by default, the option "sign" (for "integrity" and/or with password) in the method "server.state".
Travis and Heroku's GitHub integration is a comfortable option.
Istanbul is a well-known option.
Tools like audit-ci or auditjs help with this.
"npm-check-updates" automates it for you.
Use this ESLint rule : "detect-unsafe-regex".
The solution is to use "npm shrinkwrap".
Have in account that sometimes we need to assume risks.
Drop not needed stuff as much ass possible, keep it simple. Less surface exposure -> more secure.
Apply this methodology ;).
Internal: To know how to conduct a pentest it's not our responsibility as Backend developers. But of course we know about web technologies so it's something we can do for sure. We can learn at the same time we mitigate some of the vulnerabilities that are going to be found in the next step (they always find stuff:)). The best point to start (and the same the professionals use) is the OWASP Testing guide. Some free tools which can help to automate it are: ZAP, sqlmap, Skipfish, w3af, Nikto.
External: Again just hire a proper company.
This work is licensed under a Creative Commons Attribution 4.0 International License
38 commits
JavaScript
100.0%
:muscle: Exhaustive checklist to assist in a security review of a Node.js web service code. Focused on Express and Hapi environments.
The next documents have been using as main references:
Related: :skull_and_crossbones: Awesome Node.js for penetration testers
Express: By default (if not in "production" mode) exposes the stack trace of the error to the client.
Hapi: Not exposed by default.
Express:
Create HTTP errors for Express, Koa, Connect, etc. with ease".
Hapi:
- So what's the best way of dealing with uncaught exceptions? There are many opinions floating around on this.
- You application shouldn't have uncaught exceptions. This is clearly insane.
- You should let your application crash, find uncaught exceptions and fix them. This is clearly insane.
- You should swallow errors silently. This is what lots of people do and it is bad.
- You should let your application crash, log errors and restart your process with - something like upstart, forever or monit. This is pragmatic.
- You should start using domains to handle errors. Clearly the way to go, although this is an experimental feature of Node.
Hapi: Poop: "hapi plugin for handling uncaught exceptions".
Heroku: Auto-restart through the Dyno crash restart policy.
Express/Hapi: See point 1.4
Express: Enabled by default, two options:
app.disable('x-powered-by').Hapi: Disabled by default.
Express: The middleware "body-parser" provides a comfortable mechanism to support this.
Hapi: Multiple options can be set for an specific route ("parse" option).
If parsing is enabled and the 'Content-Type' is known (for the whole payload as well as parts), the payload is converted into an object when possible
Express: Use Helmet middleware ("nocache" plugin). Remember to also disable the Etag ("noEtag").
Hapi: All kind of cache disabled by default, just confirm the code it's not using anyone included here. They can also be enabled for an specific route.
Express: Use Helmet middleware ("xssFilter" plugin) to improve protection in new browsers.
Hapi: Disabled by default, the framework supports it through the route options "security" ("xss" field).
Handlebars: The function "escapeExpression" is used by default.
Handlebars HTML-escapes values returned by a {{expression}}. If you don't want Handlebars to escape a value, use the "triple-stash", {{{"*).
Dust.js: Enabled by default.
All output values are escaped to avoid Cross Site Scripting (XSS) unless you use filters"*).
Swig: The options "autoescape" is needed.
Always use an validator for all the parameters used in each route:
Express: express-validator
Hapi: joi
However, contextual escaping is missing in most template frameworks including Handlebars JS, React JSX, and Dust JS.
(by Yahoo’s Paranoid Labs).
Their solutions: secure-handlebars, express-secure-handlebars.
Express: Use Helmet middleware ("ienoopen" plugin). Hapi: Disabled by default, the framework supports it through the route options "security" ("noOpen" field).
ESLint rule: "strict".
ESLint rules:
ESLint rule: "no-implied-eval".
ESLint rule: "detect-child-process".
ESLint rule: "detect-non-fs-filename".
ESLint rule: "detect-non-literal-require".
ESLint rule: "detect-non-literal-regexp".
Deep explanation and demo included in the section A8 of NodeGoat tutorial.
Express: csurf
Hapi: crumb
Express: Use Helmet middleware ("csp" plugin).
Hapi: blankie module.
Express: Use Helmet middleware ("frameguard" plugin).
Hapi: Disabled by default, the framework supports it through the route options "security" ("xframe" field).
Express: Use Helmet middleware ("nosniff" plugin).
Hapi: Disabled by default, the framework supports it through the route options "security" ("noSniff" field).
The core "path" module "extname" method is a simple option.
I want to restrict a path to something within a given root dir, I usually do something like this
(from createWriteStream vulnerable to path traversal?)
var safePath = path.join(safeRoot, path.join('/', unsafePath));
ie: email, Slack, etc. (CWE-778)
By importance ("warn", "info", etc.) and/or file ("server").
Use an independent logger or a debugger with a debug level as the default to print always. So we're using the term "logger" (or "logging") to refer to both from now.
"debug": Using it all logs will have the same structure and we can change what to see using the "DEBUG" environment variable. Moreover it's the same used in Express.
ie: When you run a worker or standalone script.
Use log rotation and increate the limits until you consider it's safe. BTW the free plan of the wide used cloud services (over 48 h.) is not enough.
Look for another input (ie: session, DB) if possible or sanitize them before. The reason is to avoid an attacker impersonation and/or track covering (CWE-117)
Log encryption (or part of them, ie: using a secure hash). Please refer to next section ("Cryptography").
Specially before the user authentication (ie: sending a password).
Express: express-force-ssl
Extremely simple middleware for requiring some or all pages to be visited over SSL.
Hapi: hapi-require-https
hapi http -> https redirection for servers behind a reverse proxy.
Heroku & SSL:
Express: Use Helmet middleware ("hsts" plugin).
Hapi: Disabled by default, the framework supports it through the route options "security" ("hsts" field).
A tool that can help to find them is GitRob.
Note that the upgrade to OpenSSL 1.0.1s in Node.js v0.12.11 removed internal SSLv2 support. The change in this release was originally intended for v0.12.11. The --enable-ssl2 command line argument now produces an error rather than being a no-op."
Confirm the server is using a correct value for these options when requesting through the TLS Node.js core module: "rejectUnauthorized" (default:true),"checkServerIdentity", "secureProtocol".
A good to verify possible problems is "sslyze".
A good option: Let's Encryt, an open CA. Automatic HTTPS Certificates for Node.js.
Automatic live renewal.
On-the-fly HTTPS certificates for Dynamic DNS (in-process, no server restart).
Works with node cluster out of the box.
Free SSL (HTTPS Certificates for TLS) 90-day certificates.
Express: letsencrypt-express.
Hapi and Hapi middleware: letsencrypt-hapi.
Implement it manually, no serious solution found out there to help with this.
Express middlewares:
Hapi:
The basic idea is to use a secure method (random and enough length) to generate each user session identifier. The best option, as always, is to use a mature option.
Express:
Hapi:
Express: The connect-redis library supports the "ttl" options to set the session expiration.
Hapi:
expiresIn - relative expiration expressed in the number of milliseconds since the item was saved in the cache. Cannot be used together with expiresAt.
expiresAt - time of day expressed in 24h notation using the 'HH:MM' format, at which point all cache records expire. Uses local time. Cannot be used together with expiresIn.
staleIn - number of milliseconds to mark an item stored in cache as stale and attempt to regenerate it when generateFunc is provided. Must be less than expiresIn.
staleTimeout - number of milliseconds to wait before checking if an item is stale.
Express: The "express-session" middleware includes the methods "Session.Destroy" and "store.destroy" (persistent) to manage it in a consistent way.
Hapi: Native server cache manages it correctly through the supported storages via catbox.
Express: The "express-session" middleware offers the method "regenerate" to make it easier.
Hapi: here we have the method "generateFunc". Check point 6.1 tips to know more.
Express: The "express-session" middleware offers the option "name".
Hapi: Cookies enabled by default, the option "name" in the method "server.state".
Express: The "express-session" middleware offers the option "secure".
Hapi: Cookies enabled by default, the option "isSecure" in the method "server.state".
Express: The "express-session" middleware offers the option "httpOnly".
Hapi: Cookies enabled by default, the option "isHttpOnly" in the method "server.state".
Express: The "express-session" middleware offers the option "secret".
Hapi: Cookies enabled by default, the option "sign" (for "integrity" and/or with password) in the method "server.state".
Travis and Heroku's GitHub integration is a comfortable option.
Istanbul is a well-known option.
Tools like audit-ci or auditjs help with this.
"npm-check-updates" automates it for you.
Use this ESLint rule : "detect-unsafe-regex".
The solution is to use "npm shrinkwrap".
Have in account that sometimes we need to assume risks.
Drop not needed stuff as much ass possible, keep it simple. Less surface exposure -> more secure.
Apply this methodology ;).
Internal: To know how to conduct a pentest it's not our responsibility as Backend developers. But of course we know about web technologies so it's something we can do for sure. We can learn at the same time we mitigate some of the vulnerabilities that are going to be found in the next step (they always find stuff:)). The best point to start (and the same the professionals use) is the OWASP Testing guide. Some free tools which can help to automate it are: ZAP, sqlmap, Skipfish, w3af, Nikto.
External: Again just hire a proper company.
This work is licensed under a Creative Commons Attribution 4.0 International License
38 commits
JavaScript
100.0%