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
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, 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.
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
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.
⭐ 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
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.
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
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)
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
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.
6.12. Limit the allowed login requests of each user
TL;DR: A brute force protection middleware such as express-brute should be used inside an express application to prevent brute force/dictionary attacks on sensitive routes such as
/login based on request properties such as the user name, or other identifiers such as body parameters
Otherwise: An attacker can issue unlimited automated password attempts to gain access to privileged accounts on an application
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)
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
new Function constructor.
16. Prevent evil RegEx from overloading your single thread execution
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
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.
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
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.
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
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.
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
⭐ Appreciate the effort? Please star our project on GitHub
23. 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!
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