We’re under attack! 23+ Node.js security best practices
Collected, curated and written by: Yoni Goldberg, Kyle Martin and Bruno Scheufler
Tech reviewer: Liran Tal ( Node.js Security Working Group)
Welcome to our comprehensive list of Node.js security best practices which summarizes and curates the top ranked articles and blog posts
Few words before we start
Web attacks explode these days as security comes to the front of the stage. We’ve compiled over 23 Node.js security best practices (+40 other generic security practices) from all top-ranked articles around the globe. The work here is part of our Node.js best practices GitHub repository which contains more than 80 Node.js practices. Note: Many items have a read more link to an elaboration on the topic with code example and other useful information.
1. Embrace linter security rules
TL;DR: Make use of security-related linter plugins such as eslint-plugin-security to catch security vulnerabilities and issues as early as possible — while they’re being coded. This can help catching security weaknesses like using eval, invoking a child process or importing a module with a non string literal (e.g. user input). Click ‘Read more’ below to see code examples that will get caught by a security linter
Otherwise: What could have been a straightforward security weakness during development becomes a major issue in production. Also, the project may not follow consistent code security practices, leading to vulnerabilities being introduced, or sensitive secrets committed into remote repositories
Linting doesn’t have to be just a tool to enforce pedantic rules about whitespace, semicolons or eval statements. ESLint provides a powerful framework for eliminating a wide variety of potentially dangerous patterns in your code (regular expressions, input validation, and so on). I think it provides a powerful new tool that’s worthy of consideration by security-conscious JavaScript developers. (Adam Baldwin)
More quotes and code examples here
2. Limit concurrent requests using a middleware
TL;DR: DOS attacks are very popular and relatively easy to conduct. Implement rate limiting using an external service such as cloud load balancers, cloud firewalls, nginx, rate-limiter-flexible package or (for smaller and less critical apps) a rate limiting middleware (e.g. express-rate-limit)
Otherwise: An application could be subject to an attack resulting in a denial of service where real users receive a degraded or unavailable service.
Read More: Implement rate limiting
3. Extract secrets from config files or use packages to encrypt them
TL;DR: Never store plain-text secrets in configuration files or source code. Instead, make use of secret-management systems like Vault products, Kubernetes/Docker Secrets, or using environment variables. As a last result, secrets stored in source control must be encrypted and managed (rolling keys, expiring, auditing, etc). Make use of pre-commit/push hooks to prevent committing secrets accidentally
Otherwise: Source control, even for private repositories, can mistakenly be made public, at which point all secrets are exposed. Access to source control for an external party will inadvertently provide access to related systems (databases, apis, services, etc).
4. Prevent query injection vulnerabilities with ORM/ODM libraries
TL;DR: To prevent SQL/NoSQL injection and other malicious attacks, always make use of an ORM/ODM or a database library that escapes data or supports named or indexed parameterized queries, and takes care of validating user input for expected types. Never just use JavaScript template strings or string concatenation to inject values into queries as this opens your application to a wide spectrum of vulnerabilities. All the reputable Node.js data access libraries (e.g. Sequelize, Knex, mongoose) have built-in protection agains injection attacks
Otherwise: Unvalidated or unsanitized user input could lead to operator injection when working with MongoDB for NoSQL, and not using a proper sanitization system or ORM will easily allow SQL injection attacks, creating a giant vulnerability.
Read More: Query injection prevention using ORM/ODM libraries
⭐ Appreciate the effort? Please star our project on GitHub
5. Avoid DOS attacks by explicitly setting when a process should crash
TL;DR: The Node process will crash when errors are not handled. Many best practices even recommend to exit even though an error was caught and got handled. Express, for example, will crash on any asynchronous error — unless you wrap routes with a catch clause. This opens a very sweet attack spot for attackers who recognize what input makes the process crash and repeatedly send the same request. There’s no instant remedy for this but a few techniques can mitigate the pain: Alert with critical severity anytime a process crashes due to an unhandled error, validate the input and avoid crashing the process due to invalid user input, wrap all routes with a catch and consider not to crash when an error originated within a request (as opposed to what happens globally)
Otherwise: This is just an educated guess: given many Node.js applications, if we try passing an empty JSON body to all POST requests — a handful of applications will crash. At that point, we can just repeat sending the same request to take down the applications with ease
6. Adjust the HTTP response headers for enhanced security
TL;DR: Your application should be using secure headers to prevent attackers from using common attacks like cross-site scripting (XSS), clickjacking and other malicious attacks. These can be configured easily using modules like helmet.
Otherwise: Attackers could perform direct attacks on your application’s users, leading huge security vulnerabilities
Read More: Using secure headers in your application
7. Constantly and automatically inspect for vulnerable dependencies
TL;DR: With the npm ecosystem it is common to have many dependencies for a project. Dependencies should always be kept in check as new vulnerabilities are found. Use tools like npm audit, nsp or snyk to track, monitor and patch vulnerable dependencies. Integrate these tools with your CI setup so you catch a vulnerable dependency before it makes it to production.
Otherwise: An attacker could detect your web framework and attack all its known vulnerabilities.
Read More: Dependency security
8. Avoid using the Node.js crypto library for handling passwords, use Bcrypt
TL;DR: Passwords or secrets (API keys) should be stored using a secure hash + salt function like bcrypt
, that should be a preferred choice over its JavaScript implementation due to performance and security reasons.
Otherwise: Passwords or secrets that are persisted without using a secure function are vulnerable to brute forcing and dictionary attacks that will lead to their disclosure eventually.
9. Escape HTML, JS and CSS output
TL;DR: Untrusted data that is sent down to the browser might get executed instead of just being displayed, this is commonly being referred as a cross-site-scripting (XSS) attack. Mitigate this by using dedicated libraries that explicitly mark the data as pure content that should never get executed (i.e. encoding, escaping)
Otherwise: An attacker might store a malicious JavaScript code in your DB which will then be sent as-is to the poor clients
10. Validate incoming JSON schemas
TL;DR: Validate the incoming requests’ body payload and ensure it qualifies the expectations, fail fast if it doesn’t. To avoid tedious validation coding within each route you may use lightweight JSON-based validation schemas such as jsonschema or joi
Otherwise: Your generosity and permissive approach greatly increases the attack surface and encourages the attacker to try out many inputs until they find some combination to crash the application
Read More: Validate incoming JSON schemas
11. Support blacklisting JWT tokens
TL;DR: When using JWT tokens (for example, with Passport.js), by default there’s no mechanism to revoke access from issued tokens. Once you discover some malicious user activity, there’s no way to stop them from accessing the system as long as they hold a valid token. Mitigate this by implementing a blacklist of untrusted tokens that are validated on each request.
Otherwise: Expired, or misplaced tokens could be used maliciously by a third party to access an application and impersonate the owner of the token.
12. Prevent brute-force attacks against authorization
TL;DR: A simple and powerful technique is to limit authorization attempts using two metrics:
- The first is number of consecutive failed attempts by the same user unique ID/name and IP address.
- The second is number of failed attempts from an IP address over some long period of time. For example, block an IP address if it makes 100 failed attempts in one day.
Otherwise: An attacker can issue unlimited automated password attempts to gain access to privileged accounts on an application
Read More: Login rate limiting
13. Run Node.js as non-root user
TL;DR: There is a common scenario where Node.js runs as a root user with unlimited permissions. For example, this is the default behaviour in Docker containers. It’s recommended to create a non-root user and either bake it into the Docker image (examples given below) or run the process on this users’ behalf by invoking the container with the flag “-u username”
Otherwise: An attacker who manages to run a script on the server gets unlimited power over the local machine (e.g. change iptable and re-route traffic to his server)
Read More: Run Node.js as non-root user
14. Limit payload size using a reverse-proxy or a middleware
TL;DR: The bigger the body payload is, the harder your single thread works in processing it. This is an opportunity for attackers to bring servers to their knees without tremendous amount of requests (DOS/DDOS attacks). Mitigate this limiting the body size of incoming requests on the edge (e.g. firewall, ELB) or by configuring express body parser to accept only small-size payloads
Otherwise: Your application will have to deal with large requests, unable to process the other important work it has to accomplish, leading to performance implications and vulnerability towards DOS attacks
15. Avoid JavaScript eval statements
TL;DR: eval
is evil as it allows executing a custom JavaScript code during run time. This is not just a performance concern but also an important security concern due to malicious JavaScript code that may be sourced from user input. Another language feature that should be avoided is new Function
constructor. setTimeout
and setInterval
should never be passed dynamic JavaScript code either.
Otherwise: Malicious JavaScript code finds a way into a text passed into eval or other real-time evaluating JavaScript language functions, and will gain complete access to JavaScript permissions on the page. This vulnerability is often manifested as an XSS attack.
Read More: Avoid JavaScript eval statements
16. Prevent evil RegEx from overloading your single thread execution
TL;DR: Regular Expressions, while being handy, pose a real threat to JavaScript applications at large, and the Node.js platform in particular. A user input for text to match might require an outstanding amount of CPU cycles to process. RegEx processing might be inefficient to an extent that a single request that validates 10 words can block the entire event loop for 6 seconds and set the CPU on 🔥. For that reason, prefer third-party validation packages like validator.js instead of writing your own Regex patterns, or make use of safe-regex to detect vulnerable regex patterns
Otherwise: Poorly written regexes could be susceptible to Regular Expression DoS attacks that will block the event loop completely. For example, the popular moment
package was found vulnerable with malicious RegEx usage in November of 2017
Read More: Prevent malicious RegEx
17. Avoid module loading using a variable
TL;DR: Avoid requiring/importing another file with a path that was given as parameter due to the concern that it could have originated from user input. This rule can be extended for accessing files in general (i.e. fs.readFile()
) or other sensitive resource access with dynamic variables originating from user input. Eslint-plugin-security linter can catch such patterns and warn early enough
Otherwise: Malicious user input could find its way to a parameter that is used to require tampered files, for example a previously uploaded file on the filesystem, or access already existing system files.
Read More: Safe module loading
18. Run unsafe code in a sandbox
TL;DR: When tasked to run external code that is given at run-time (e.g. plugin), use any sort of ‘sandbox’ execution environment that isolates and guards the main code against the plugin. This can be achieved using a dedicated process (e.g. cluster.fork()), serverless environment or dedicated npm packages that acting as a sandbox
Otherwise: A plugin can attack through an endless variety of options like infinite loops, memory overloading, and access to sensitive process environment variables
Read More: Run unsafe code in a sandbox
19. Take extra care when working with child processes
TL;DR: Avoid using child processes when possible and validate and sanitize input to mitigate shell injection attacks if you still have to. Prefer using child_process.execFile which by definition will only execute a single command with a set of attributes and will not allow shell parameter expansion.
Otherwise: Naive use of child processes could result in remote command execution or shell injection attacks due to malicious user input passed to an unsanitized system command.
Read More: Be cautious when working with child processes
20. Hide error details from clients
TL;DR: An integrated express error handler hides the error details by default. However, great are the chances that you implement your own error handling logic with custom Error objects (considered by many as a best practice). If you do so, ensure not to return the entire Error object to the client, which might contain some sensitive application details
Otherwise: Sensitive application details such as server file paths, third party modules in use, and other internal workflows of the application which could be exploited by an attacker, could be leaked from information found in a stack trace
Read More: Hide error details from clients
21. Configure 2FA for npm or Yarn
TL;DR: Any step in the development chain should be protected with MFA (multi-factor authentication), npm/Yarn are a sweet opportunity for attackers who can get their hands on some developer’s password. Using developer credentials, attackers can inject malicious code into libraries that are widely installed across projects and services. Maybe even across the web if published in public. Enabling 2-factor-authentication in npm leaves almost zero chances for attackers to alter your package code.
Otherwise: Have you heard about the eslint developer who’s password was hijacked?
22. Modify session middleware settings
TL;DR: Each web framework and technology has its known weaknesses — telling an attacker which web framework we use is a great help for them. Using the default settings for session middlewares can expose your app to module- and framework-specific hijacking attacks in a similar way to the X-Powered-By
header. Try hiding anything that identifies and reveals your tech stack (E.g. Node.js, express)
Otherwise: Cookies could be sent over insecure connections, and an attacker might use session identification to identify the underlying framework of the web application, as well as module-specific vulnerabilities
Read More: Cookie and session security
23. Avoid DOS attacks by explicitly setting when a process should crash
TL;DR: The Node process will crash when errors are not handled. Many best practices even recommend to exit even though an error was caught and got handled. Express, for example, will crash on any asynchronous error — unless you wrap routes with a catch clause. This opens a very sweet attack spot for attackers who recognize what input makes the process crash and repeatedly send the same request. There’s no instant remedy for this but a few techniques can mitigate the pain: Alert with critical severity anytime a process crashes due to an unhandled error, validate the input and avoid crashing the process due to invalid user input, wrap all routes with a catch and consider not to crash when an error originated within a request (as opposed to what happens globally)
Otherwise: This is just an educated guess: given many Node.js applications, if we try passing an empty JSON body to all POST requests — a handful of applications will crash. At that point, we can just repeat sending the same request to take down the applications with ease.
24. Prevent unsafe redirects
TL;DR: Redirects that do not validate user input can enable attackers to launch phishing scams, steal user credentials, and perform other malicious actions.
Otherwise: If an attacker discovers that you are not validating external, user-supplied input, they may exploit this vulnerability by posting specially-crafted links on forums, social media, and other public places to get users to click it.
Read more: Prevent unsafe redirects
25. Avoid publishing secrets to the npm registry
TL;DR: Precautions should be taken to avoid the risk of accidentally publishing secrets to public npm registries. An .npmignore
file can be used to blacklist specific files or folders, or the files
array in package.json
can act as a whitelist.
Otherwise: Your project’s API keys, passwords or other secrets are open to be abused by anyone who comes across them, which may result in financial loss, impersonation, and other risks.
Read More: Avoid publishing secrets
⭐ Appreciate the effort? Please star our project on GitHub
26. A list of 40 generic security advice (not specifically Node.js-related)
The following bullets are well-known and important security measures which should be applied in every application. As they are not necessarily related to Node.js and implemented similarly regardless of the application framework — we include them here as an appendix. The items are grouped by their OWASP classification. A sample includes the following points:
- Require MFA/2FA for root account
- Rotate passwords and access keys frequently, including SSH keys
- Apply strong password policies, both for ops and in-application user management, see OWASP password recommendation
- Do not ship or deploy with any default credentials, particularly for admin users
- Use only standard authentication methods like OAuth, OpenID, etc. — avoid basic authentication
- Auth rate limiting: Disallow more than X login attempts (including password recovery, etc.) in a period of Y minutes
- On login failure, don’t let the user know whether the username or password verification failed, just return a common auth error
- Consider using a centralized user management system to avoid managing multiple account per employee (e.g. GitHub, AWS, Jenkins, etc) and to benefit from a battle-tested user management system
The complete list of 40 generic security advice can be found in the official Node.js best practices repository!
Read More: 40 Generic security advice
Other good reads:
- Node.js production best practices — Yoni Goldberg
- Node.js Security Overview — Gergely Nemeth
- Express security best practices — Express official
- YouTube: A Node.js Security Roadmap — Mike Samuel
Authors — about us
- Yoni Goldberg — Node.js consultant, serving customers in USA, Europe and Israel
- Kyle Martin - Full Stack Developer based in New Zealand
- Bruno Scheufler — Full-stack web developer and Node.js enthusiast
⭐ Appreciate the effort? Clapping at the bottom (up to 50 times) can make our day