Home

Awesome

Strong Node.js

: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:

<div align="center"> <p> <img src="https://i.ibb.co/7vqSK2C/undraw-to-do-list-a49b.png" alt="checklist"> </p> </div>

Related: :skull_and_crossbones: Awesome Node.js for penetration testers

1 Errors

1.1 Returned errors don't include sensitive information about the user or other user's info (CWE-209)

1.2 Returned errors don't include sensitive information about the environment: stack, paths, DB queries, etc (CWE-209)

1.2.1 The environment variable "NODE_ENV" environment variable is set to "production"

Express: By default (if not in "production" mode) exposes the stack trace of the error to the client.

Hapi: Not exposed by default.

1.3 Default framework errors are never returned (CWE-209)

1.4 A custom error page is defined (CWE-756)

Express:

Hapi:

1.5 The app takes care of "uncaughtException" events to avoid a the application stop ("denial of service" - CWE-248)

1.6 The app takes care of "unhandledRejection" events. The same idea but with promises (CWE-248)

Hapi: Poop: "hapi plugin for handling uncaught exceptions".

Heroku: Auto-restart through the Dyno crash restart policy.

1.7 The content of the errors should avoid to reason about any internal state of the application (CWE-203)

Express/Hapi: See point 1.4

1.8 The time to return an error should avoid to reason about any internal state of the application" (CWE-208)

1.9 All dependencies generated errors also respect the points of this section (CWE-211)

2 Input and output

2.1 The HTTP header "x-powered-by" is disabled in the responses

Express: Enabled by default, two options:

Hapi: Disabled by default.

2.2 The encoding is correctly set for all routes (CWE-172)

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

2.3 Inputs with sensitive data are never auto-completed/cached in the browser (CWE-524)

2.4 The HTTP header "Cache-Control" is disabled in the responses (CWE-524)

2.5 The HTTP header "Etag" is disabled in the responses (CWE-524)

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.

2.6 The header "x-xss-protection" is being set (CWE-79)

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).

2.7 Escaping potentially untrusted inputs is applied for all user entries (CWE-79, CWE-77, CWE-89)

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

2.8 Context-sensitive output escaping is applied for all output values (CWE-79)

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.

2.9 For IE specific output escaping is applied for all output values

Express: Use Helmet middleware ("ienoopen" plugin). Hapi: Disabled by default, the framework supports it through the route options "security" ("noOpen" field).

2.10 The app uses parametrized database queries if a SQL database is being used (CWE-89)

2.11 The "strict" options is used in the whole code, to apply more defenses (CWE-77)

ESLint rule: "strict".

2.12 App code doesn't use "eval" at all (CWE-77)

ESLint rules:

2.13 App doesn't use any method which leads to the same result as "eval" using user inputs (CWE-77)

ESLint rule: "no-implied-eval".

2.14 App doesn't use any method of the "childProcess" object using user inputs (CWE-77)

ESLint rule: "detect-child-process".

2.15 Non-literals are not allowed in any method of the "fs" module as a name (CWE-77)

ESLint rule: "detect-non-fs-filename".

2.16 Non-literals are not allowed in any "require" (CWE-77)

ESLint rule: "detect-non-literal-require".

2.17 Non-literals are not allowed in any regular expression (CWE-77)

ESLint rule: "detect-non-literal-regexp".

2.18 Protection against Cross-Site Request Forgery (CSRF) is enabled (CWE-352)

Deep explanation and demo included in the section A8 of NodeGoat tutorial.

Express: csurf

Hapi: crumb

2.19 The Content Security Policy setup is correct (CWE-352)

Express: Use Helmet middleware ("csp" plugin).

Hapi: blankie module.

2.20 The "xframe" field is used, to avoid clickjacking (CWE-693)

Express: Use Helmet middleware ("frameguard" plugin).

Hapi: Disabled by default, the framework supports it through the route options "security" ("xframe" field).

2.21 The app is adding headers to avoid the browsers sniffing mimetypes (CWE-430)

CVE-2014-7939 Detail

Express: Use Helmet middleware ("nosniff" plugin).

Hapi: Disabled by default, the framework supports it through the route options "security" ("noSniff" field).

2.22 All uploaded files extension is checked the extension to be among the supported ones (CWE-434)

The core "path" module "extname" method is a simple option.

2.23 All unsafe paths names are restricted to a root dir (CWE-22)

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));

3 Auditing and logging

3.1 All critical errors being logged, at any level of debugging (CWE-778)

3.2 An alert is generated when a critical error happens

ie: email, Slack, etc. (CWE-778)

3.3 Different levels of debugging are supported (CWE-778)

By importance ("warn", "info", etc.) and/or file ("server").

3.4 Change the debug level without restart, ie: using environment variables (CWE-778)

3.5 All security-critical events are being logged (CWE-778)

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.

3.6 All authentication activities (successful or not) are being logged, at any level of debugging (CWE-223, CWE-778)

3.7 All privilege changes (successful or not) are being logged, at any level of debugging (CWE-223, CWE-778)

3.8 All administrative activities (successful or not) are being logged, at any level of debugging (CWE-223, CWE-778)

ie: When you run a worker or standalone script.

3.9 All access to sensitive data are being logged, at any level of debugging (CWE-223, CWE-778)

3.10 An alert is generated when a critical security event happens, ie: email, Slack, etc. (CWE-223, CWE-778)

3.11 Anomalous conditions can be easily detected through the logs (CWE-779)

3.12 All errors are logged at the same level (CWE-779)

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.

3.13 An user entry is never written directly to the logs

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)

3.14 Logs never change the app behavior (CWE-117)

3.15 Statistics are not taken from the logs (CWE-117)

3.16 Logs do not include sensitive info about users or environment. (CWE-532, CWE-215)

Log encryption (or part of them, ie: using a secure hash). Please refer to next section ("Cryptography").

3.17 Logs location is secure (CWE-533)

4 Cryptography

4.1 All routes which transmit sensitive info use SSL (CWE-523, CWE-311, CWE-319).

Specially before the user authentication (ie: sending a password).

4.2 HTTP access is disabled for all routes which use SSL (CWE-523, CWE-311, CWE-319)

Express: express-force-ssl

Hapi: hapi-require-https

Heroku & SSL:

4.3 The server only allows SSL connections (RFC 6797)

Express: Use Helmet middleware ("hsts" plugin).

Hapi: Disabled by default, the framework supports it through the route options "security" ("hsts" field).

4.4 The passwords, keys or certificates are not stored in clear files (CWE-312, CWE-319)

4.5 The passwords, keys or certificates are not stored in clear in the DB (CWE-312, CWE-319)

A tool that can help to find them is GitRob.

4.6 Passwords, keys or certificates are not stored in a recoverable format (CWE-261, CWE-257)

4.7 The app is using bcrypt or pbkdf2 (or based library) to store the passwords securely (CWE-326)

4.8 The app is using secure crypto libraries (CWE-327)

4.9 The app certificate respect the chain of trust (CWE-296, CWE-295)

4.10 The app certificate match with the host (CWE-297, CWE-295, CWE-322)

4.11 The app certificate has a valid expiration date (CWE-298, CWE-295)

4.12 The app certificate is not revoked (CWE-299, CWE-295)

4.13 All the points of this section are checked for any application SSL connections

Express: letsencrypt-express.

Hapi and Hapi middleware: letsencrypt-hapi.

5 Authentication and authorization

5.1 Neither passwords nor certificate keys are hard-coded (or in separate file) in the source code of the application (CWE-798)

5.2 The password recovery mechanism is strong (CWE-640)

5.3 The users are forced to enter a strong password (CWE-521)

5.4 The users are forced to change the password in a regular basis (CWE-262)

Implement it manually, no serious solution found out there to help with this.

5.5 The application detects and blocks any possible brute-force attack (CWE-307)

5.6 The block expires after a period of time (CWE-307)

5.7 The application support a blacklist of IP address to block (CWE-307)

5.8 The application can manually block an IP address (CWE-307)

5.9 The application can manually unblock an IP address (CWE-307)

5.10 The application can manually block a country (CWE-307)

5.11 The application can manually unblock a country (CWE-307)

Express middlewares:

Hapi:

5.12 All requests came through an authentication middleware (CWE-284)

5.13 All new requests (not login users) have the least privilege possible (CWE-272, CWE-284)

5.14 All the info taken in account for authentication is taken from trusted sources (CWE-284)

5.15 The app doesn't expose actual database keys as part of the access links (CWE-284)

5.16 The app doesn't use URL redirection (CWE-601)

5.16.1 If URL redirection is used, it doesn’t involve user parameters to calculate the destination

6 Session

6.1 The method to generate session IDs is strong (CWE-6)

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:

6.2 The session is destroyed on every user logout (CWE-613)

6.3 The session is destroyed after an absolute session timeout (CWE-613)

6.4 The session is destroyed after an iddle session timeout (CWE-613)

6.5 In all cases the sessions is also dropped from persistent storage (ie: Redis) (CWE-613)

Express: The connect-redis library supports the "ttl" options to set the session expiration.

Hapi:

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.

6.6 The server generate a new session ID after an user authentication (CWE-384)

6.7 The server generate a new session ID after an user privilege level change (CWE-384)

6.8 The server generate a new session ID after an encryption level change (CWE-384)

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.

6.9 All cookies have a not default name

Express: The "express-session" middleware offers the option "name".

Hapi: Cookies enabled by default, the option "name" in the method "server.state".

6.10 All cookies use the "secure" flag, to set them only under HTTPS. (CWE-614)

Express: The "express-session" middleware offers the option "secure".

Hapi: Cookies enabled by default, the option "isSecure" in the method "server.state".

6.11 All cookies use the "HttpOnly" flag, to ensures they are only sent over HTTP(S), not client JavaScript. (CWE-79)

Express: The "express-session" middleware offers the option "httpOnly".

Hapi: Cookies enabled by default, the option "isHttpOnly" in the method "server.state".

6.12 All cookies are signed with a secret (CWE-565)

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".

7 Environment

7.1 The app has a CI system (CWE-439, CWE-701, CWE-656)

Travis and Heroku's GitHub integration is a comfortable option.

7.2 The coverage for the tests is enough

Istanbul is a well-known option.

7.3 Check for dependencies with known vulnerabilities is included in the CI

Tools like audit-ci or auditjs help with this.

7.4 Check for non updated dependencies is included in the CI

"npm-check-updates" automates it for you.

7.5 Check for insecure regular expressions is included in the CI. (CWE-185)

Use this ESLint rule : "detect-unsafe-regex".

7.6 Semantic versioning is used correctly

7.7 Dependency versions are blocked in production to avoid surprises

The solution is to use "npm shrinkwrap".

7.8 There's a npm task to install dependencies ignoring the scripts (VU#319816)

7.9 The user inputs are fuzzed in a regular basis

7.10 The whole infrastructure is secured (CWE-15, CWE-656)

7.11 Application with minimal privileges (CWE-250)

7.12 The team in educated on security

7.13 The team doesn't use company devices for personal stuff

7.14 The app respect some written security requirements

Have in account that sometimes we need to assume risks.

7.15 The application has an incident plan

7.16 A design review is performed in a regular basis

Drop not needed stuff as much ass possible, keep it simple. Less surface exposure -> more secure.

7.17 A security code audit is performed regular basis (internal and external). (CWE-702)

Apply this methodology ;).

7.18 A web specific penetration test is performed in a regular basis (internal and external)

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.

License

Creative Commons License

This work is licensed under a Creative Commons Attribution 4.0 International License