Want to try it for yourself?
Node.js itself is a secure platform — but many third-party open source packages in the ecosystem may not be. These packages are used through Node Package Manager (NPM) and are susceptible to many different types of vulnerabilities and other Node.js licensing and security risks. Some Node.js code bases have thousands of these packages, opening up the housed applications to a lot of risks.
Why should you secure Node.js?
NPM (which is usually used with the Node.js runtime) is one of the largest open source package ecosystems. Open source technologies can be of major concern when it comes to cybersecurity. In fact, Snyk’s State of Open Source Security report shows that the average project has 49 vulnerabilities spanning 79 direct dependencies. In addition, a developer on GitHub ran a security experiment where they were able to gain access to 14% of NPM packages, with another 54% potentially reachable through dependency chains.
This demonstrates that Node.js is not secure enough for developers to close the book on potential exposure to hackers. In addition to these ten NPM security best practices, we recommend the additional practices outlined below to ensure the security of applications built on Node.js.
Like any application, those built with Node.js come with security risks. The following top five — and our top ten best practice recommendations to follow — are important takeaways from the OWASP Top Ten Vulnerabilities and Application Security Risks.
1.Cross-site scripting (XSS)
2. Cross-site request forgery (CSRF)
A cross-site request forgery attack hijacks user sessions by hiding malicious code under seemingly trustworthy HTML elements. Because the user is already logged in and authenticated, clicking one of these masked links gives the hacker the ability to execute changes in the underlying systems.
3. Code injection
Attackers can use an input validation flaw to inject malicious code into your codebase, changing the way your application executes. Code injection can give them access to sensitive data, provide information about your environment, or infect your system with malware.
4. Distributed denial of service (DDoS) attacks
In a DDoS attack, an attacker floods production servers with internet traffic to disrupt their normal function. This traffic can overwhelm the system and cause significant damage and outages. Versions 4-4.1.1 of Node.js that contained a bug with the HTTP handling are one example of this.
5. Regular expression denial of service attacks (ReDos)
This type of denial-of-service (DoS) attack can take a system down by providing an input that makes it time-consuming for the program to evaluate a regular expression. This slows or even halts the program and produces a DoS to legitimate users.
To combat the most common Node.js risks, you need to put some important best practices in place. Here are the ten most valuable best practices that can help you avoid common security pitfalls in Node.js.
Logging and monitoring are incredibly important for consistent security in Node.js. Monitoring your logs gives you insight into what is happening in your application so you can investigate anything suspicious. A few levels that are important to log include info, error, warn, and debug. To cut down on manual effort, you can leverage modules like Bunyan and toobusy-js to automate logging and monitoring.
You may have noticed that several of the potential attacks identified above involve a malicious actor bypassing or exploiting user authentication. Having strong authentication policies in place is an important safeguard against these attackers. Here are a few authentication guidelines:
Require multi-factor authentication (MFA) and single sign-on (SSO)
Use the Scrypt or Bcrypt libraries over the Node.js crypto library
Implement consistent session-handling policies
Require strong user passwords
Restrict the number of failed login attempts
When it comes to your event loop and worker pool threads, executing heavy activity on either one can halt response requests from other clients — and negatively affect your throughput. Here are some ways to ensure your threads stay open:
Reduce the complexity of your callbacks by keeping the number of steps consistent, regardless of the argument. This will give every client in the queue a fair shot.
If you have a complex task, bound the input and reject lengthy inputs. Bound inputs will only execute against the timeframe of your worst-case input, ensuring that the thread won’t block (of course, you must first make sure your worst-case input time frame is short enough to avoid blocking the thread).
Avoid using synchronous APIs from the encryption, compression, file system, or child process modules. They have extensive completion times and can block your event loop.
A lot of application performance and security issues can be mitigated by prepping code to handle errors. Here are some best practices to keep your programs bug-free:
Instead of managing asynchronous errors with callbacks, use a reputable promise library or async-await.
Customized or string error handling introduces complexity; simplify errors by using the built-in error object every time.
Handling errors in middleware can lead to code duplication — error handling logic is best managed in a centralized location.
Test your error flows regularly so that you know they will not throw out false positives or outright misses.
When sending information to the front-end, be sure you’re only sending essential data. While, in theory, you can filter any information you wish from being displayed, attackers can still access hidden data through the back-end. The only way to ensure they can’t access sensitive data is to simply not send it unless absolutely necessary.
The default limit for requests in Node.js is 5MB. To avoid a DDoS attack where a hacker floods your servers, you can further limit the size of the requests Node.js will allow.
Here are two simple ways to accomplish this:
Configure the ‘limit’ option in the body-parser package to only accept small payloads.
Use reverse proxies or express middleware to set size limits for certain types of content.
One of the reasons malicious actors can achieve successful XSS attacks is because user input is not being validated. The first thing to do in this scenario is sanitize your user input with an NPM package like DOMPurify. From there, use a form validation library like express-validator or XSS-filters. Form validation libraries automate the monotonous task of validating each input in each request. Context-dependent output filters like XSS-filters are developer-friendly and encode the minimal number of characters needed to thwart an XSS attack.
Node.js does not offer advanced forms of object serialization. Because of this, attackers can use serialized objects to transfer payloads. Put integrity and user authentication checks in place and sanitize deserialized data to avoid this situation. On a more granular level, use anti-CSRF tokens, custom request headers, the SameSite flag in cookie sessions, and user interaction-based protection.
In order to fix vulnerabilities that could lead to an attack, you must first know where the vulnerabilities are. Using tools like linter plugins and static application security testing (SAST) can automate this process so you don’t miss anything.
Linters like eslint-plugin-node-security analyze source code to identify and bubble up any insecure practices. SAST tools like Snyk Code monitor your Node.js code as you write and alert you of any potential vulnerabilities.
This is a good practice to use across the board, but particularly in Node.js, it’s important that you don’t run it as a root user. By running Node.js as a non-root user, you’re limiting the potential attack surface that malicious actors can take advantage of.
This recommendation follows the principle of least privilege — only give users the exact amount of access they need to do their job. Root access should only be granted in very specific situations.
Since May of 2021, Snyk has contributed to the security of the Node.js ecosystem by taking over the Node.js ecosystem vulnerability disclosure program. Snyk’s dedicated team of security analysts and researchers triage Node.js disclosure reports, verify them, and reach out to the maintainers of the reported packages to begin the responsible disclosure process as per Snyk’s disclosure policy.
In addition, Snyk products enable you to find and fix vulnerabilities in five minutes. Here’s how Snyk improves security in Node.js:
Snyk Open Source helps identify and alert developers of vulnerabilities within the dependencies of a Node.js application. This helps ensure that your code is protected from the top security risks we’ve covered today.
Snyk Code is a SAST tool that helps identify and alert developers of vulnerabilities found within their application code.
Snyk Container helps developers find vulnerabilities in containers and Kubernetes workloads throughout the SDLC.
Note that if you are using Node.js in containers, we have additional best practices we recommend to containerize Node.js applications with Docker.
Is Node.js secure?
The Node.js platform is inherently secure, but because it uses third-party open source packages through its package management system (npm), it is vulnerable to cyber attacks. Companies must implement the best practices like those outlined in this article to maintain the security of Node.js.
How can you secure Node.js?
Establishing practices like validating user input, implementing authentication, limiting request sizes, and setting up logging and monitoring are good first steps to securing Node.js. For specific concerns about code injection attacks, here are five ways to prevent code injection in Node.js.
Can Node.js be hacked?
Code security auditing 101
A code security audit is a process of analyzing source code with the goal of finding security vulnerabilities, licensing, and other programming issues. §Keep reading