💪 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:
- The SANS SWAT (Securing Web Applications Technologies) checklist.
- The CWE (Common Weakness Enumeration) dictionary.
Related: ☠️ Awesome Node.js for penetration testers
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)
Express: By default (if not in "production" mode) exposes the stack trace of the error to the client.
Hapi: Not exposed by default.
1.4 A custom error page is defined (CWE-756)
- They could allow an attacker to detect it as part of a more sophisticated attack.
- Define a custom error handler. The way to achieve this is using a middleware.
Express:
- The Default Error Handler
- http-errors:
Create HTTP errors for Express, Koa, Connect, etc. with ease".
Hapi:
- Custom implementations: https://stackoverflow.com/a/28044412/2087521, https://stackoverflow.com/a/32185406/2087521
- Boom: Easy consistent HTTP errors.
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)
- This kind of exceptions mean that the application is in an undefined state. So don't try to simply restart the application again without properly recovering from the exception or fixing the bug. This could head to additional unforeseen and unpredictable issues.
- 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.
- (from Uncaught Exceptions in Node.js)
- Last two cases can be considered safe.
- Further reading: About how to handle errors by Joyent -> https://www.joyent.com/developers/node/design/errors
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)
- A common example is a response like: "Invalid login" vs. "User not found". If we use the second we are allowing a possible brute-force of our usernames.
- Some modules can help to send consistent ones (easier to reason about).
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)
- A common example is when the combination "username/password" is "good/bad" vs "bad/bad".
- To detect it:
- Use this ESLint rule : "detect-possible-timing-attacks".
- Some tools to automate it: nanown, time_trial
- To prevent it use secure libraries to compare like:
- "cryptiles" (Hapi project)
- "credential"
- "safe-compare"
1.9 All dependencies generated errors also respect the points of this section (CWE-211)
- Control all our errors, massaging them if needed to avoid the risks commented in these last points.
- Use mature modules.
- Use secure modules. Add checks to your development workflow with a tool like audit-ci or auditjs.
- To avoid the framework fingerprinting.
Express: Enabled by default, two options:
- Use
app.disable('x-powered-by')
. - Use Helmet middleware ("hide-powered-by" plugin).
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)
- Use the HTML input "autocomplete" Attribute in the client side to avoid be cached by the browser.
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).
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.
- "Blindly-escaping is insufficient" (by Yahoo)
- Reducing XSS by way of Automatic Context-Aware Escaping in Template Systems (Google, 2009)
- Deep explanation and demo included in the section A3 of NodeGoat tutorial.
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)
- Use escaped values:
- Deep explanation and demo included in the section A1 (section B) of NodeGoat tutorial.
2.11 The "strict" options is used in the whole code, to apply more defenses (CWE-77)
ESLint rule: "strict".
ESLint rules:
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".
ESLint rule: "detect-non-literal-require".
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)
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.1 All critical errors being logged, at any level of debugging (CWE-778)
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.
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.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)
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)
- To avoid a possible modification from attacker writing in them.
- Use secure tools to inspect automate the log inspection.
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)
- We also need to store these server logs in a secure environment.
- A cloud service is comfortable here, this way you transfer the risk to them.
- If the log service has a management panel, a 2-factor authentication mechanism is mandatory.
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:
- Addon: Expedited SSL
- HowTo: https://www.youtube.com/watch?v=OcyR7Yus4pc
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)
A tool that can help to find them is GitRob.
- Simply don't roll your own crypto!, except for fun/learn. All frameworks include their own solutions to manage the login system and npm is full of them, just use one mature enough. Even we have other good standalone options, like the Passwordless middleware.
- An interesting option is to avoid using passwords at all (CWE-309). Most of actual web applications connect user social accounts, so I think it's better to rely on them using a mature module (Passport or Bell). Better in one with mechanisms to figth against fake profiles.
4.7 The app is using bcrypt or pbkdf2 (or based library) to store the passwords securely (CWE-326)
- Remember, in general, MD5, SHA1, SHA256, SHA512, SHA-3, etc are not fully secure to other thing but check the integrity of the data. it's better to use bcrypt or pbkdf2 based libraries.
- How To Safely Store A Password. The main advantage of pbkdf2 is to be platform independent.
4.8 The app is using secure crypto libraries (CWE-327)
- More specific:
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."
- Use an updated version of Node.js.
- Refer to point 3.4 tips.
- Just in case, check TLS Node.js core module "secureProtocol" option which is being used.
- To exclude one cipher from the allowed ones use the option "cipher" in the TLS module.
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)
-
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.
5.1 Neither passwords nor certificate keys are hard-coded (or in separate file) in the source code of the application (CWE-798)
- A tool that can help again is GitRob.
- The correct way to avoid hardcoding the credentials of the different microservices is to use environment/application variables. Only the one in charge of the deployment should have access to get/set them through a secure channel.
5.2 The password recovery mechanism is strong (CWE-640)
- Please, refer to the point 4.6 of this document, just use a mature option.
- An important specific check is to validate the security of the generated token sent to the user email.
5.3 The users are forced to enter a strong password (CWE-521)
- We should ask them for a new better option if it doesn't reach our expectations.
- Please, refer to the point 4.6, use a mature solution.
- By the way we have some good modules to check the strength of the passwords set by the users.
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)
- node-ratelimiter: It uses Redis, supports reset.
- To locate an IP address: node-geoip, but there're multiple options.
Express middlewares:
- node-ipgeoblock: "blacklist of IPs, the blacklist of countries or the whitelist of countries.""
- express-limiter: Built on Redis, whitelists support.
- express-brute: Support multiples stores, "increasing the delay with each request in a fibonacci-like sequence".
Hapi:
- hapi-ratelimit: Built on Redis.
- Manual implementation: https://gist.github.com/whisher/d6e3db7c11d632720133
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)
- A common example is to get an user ID from "req.params.userId" (which could be manipulated directly in the request payload) instead "req.session.userId".
- Deep explanation and demo included in the section A4 of NodeGoat tutorial.
- The quickest and most secure option here is to use a well-tested framework to implement this part, ie: Passport.
- Map each internal ID to a shorter (and more beautiful) one before sending it to the client and use it for the whole communication.
- "shortid": "Short id generator. Url-friendly. Non-predictable. Cluster-compatible.".
- Deep explanation and demo included in the section A7 of NodeGoat tutorial.
5.16 The app doesn't use URL redirection (CWE-601)
- In general, avoid using URL redirection.
- It is recommended that any destination parameters is a mapped value, instead of the actual URL (or a part).
- Deep explanation and demo included in the section A10 of NodeGoat tutorial.
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:
- A secure method is used ("uid-safe" module) by default in the "express-session" middleware.
- Moreover the user can define his own one using the "genid" option.
Hapi:
- The methods server.cache includes the method "generateFunc" to specify your own algorithm.
- Another of the most used options ("hapi-auth-basic", "hapi-auth-cookie" and "yar").
- Neither of them don't include a method to generate it by default. So the user have to implement his own using a secure alternative (again, like "uid-safe").
6.2 The session is destroyed on every user logout (CWE-613)
- Inactive sessions should be destroyed automatically when the user is inactive from an amount of time. There are multiple JavaScript libraries to achieve it from the client-side.
Express: The connect-redis library supports the "ttl" options to set the session expiration.
Hapi:
- The server side caching is a built-in feature. It includes the method "server.state" also includes the option "ttl". Moreover it also includes the "server.cache" one which supports the following options:
-
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.
-
- Another important check is to confirm (to avoid pollution and future space problems) that the session is also destroyed in the permanent storage (ie: Redis).
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)
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".
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".
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.
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
- To support the last version of the dependencies.
- Use the tilde and caret correctly in the "package.json" file.
The solution is to use "npm shrinkwrap".
7.8 There's a npm task to install dependencies ignoring the scripts (VU#319816)
- The solution is to block them. So it's better to have an npm/Grunt/Gulp task to do it: Package install scripts vulnerability
- A library to help: Surku
- Use external libraries like ZAP (Node.js bindings).
- Attackers should use the easiest way to break in, so we also need to secure the OS where the service is running.
- Cloud deployment platforms are a good option to minimize this risk.
- In case you prefer to manage your own OS a good tool to check automatically its security in an easy way is Lynis.
7.11 Application with minimal privileges (CWE-250)
- All involved microservices should respect this rule.
- Again a comfortable solution is to use a cloud deployment environment to avoid this risk.
- Sending them to conferences is a fun way to achieve it with good results.
- Official advisories: https://groups.google.com/forum/#!forum/nodejs-sec.
- It's good to watch the Github repositories for notifications to be informed if any vulnerabilities are discovered in the package in future.
Have in account that sometimes we need to assume risks.
- Which includes how to recover from the worst case scenarios (ie: Amazon -> Heroku, GitHub down).
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 ;).
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