Top 20 Node.js Security Best Practices: Potential Risks and Their Solutions
When it comes to web application development or any other development projects, open-source technologies have become the first choice for the developer community across the globe. In fact, 76% of the people in 2021 have utilized open-source technologies for software development projects. However, security remains to be one of the prime concerns for developers who utilize these open-source technologies.
Node.js is one such technology that developers use for web application development. It is designed to be completely secure. However, while developing any web application through Node.js, you will need to utilize various third-party open-source packages through NPM (Node Package Manager) and unfortunately, as per a survey, 14% of the NPM ecosystem is vulnerable to security issues, and these indirectly account for 54% of the packages. Ultimately, these vulnerabilities are transferred to Node.js web applications.
Security vulnerabilities are not new for open-source backend frameworks and every Node.js developer understands the risks that hackers can pose for applications and user data. Keeping Node.js security risks in mind, the article would focus on the risks, best practices and tools development teams can utilize for improving their web application security.
Editor’s note: In this article, the editor talks about Node.js, security vulnerabilities, best practices and tools that development teams can utilize for improving their web application security. If you are interested in professional assistance to optimize your web app or build one that follows security best practices, feel free to check out our custom web application development offerings.
Top Node.js security risks
Writing a secure code for an application is a developer’s primary responsibility. However, when using open-source packages, you can’t entirely guarantee your codebase’s security. Code injection refers to any attack where the attacker injects the code into the system and forces the application routine to execute it. The attacker explores the poorly handled and untrusted data to gain insights into your codebase.
A common reason for this security risk is improper input and output data validation. SQL injection is a recurring code injection attack most people encounter during software development. Here, the attacker uses the malicious SQL code to manipulate the backend database and gain access to sensitive information which is not generally visible.
2. Cross-site request forgery (CSRF) attack
Cross-Site Request Forgery (CSRF) is a common Node.js security vulnerability you should not ignore. CSRF attack forces the authenticated users to submit a request to a web application against which they are already authenticated. It allows attackers to access sensitive data and comprise the privacy and integrity of web applications.
The primary aim of CSRF attackers is to change the state of the application by using social engineering techniques like sending a message or an email to the users. CSRF attacks can cause significant damage to Node.js apps as it forces users to change their email addresses and transfer funds. For admin-level users, CSRF attacks can also compromise the entire web application security and must be mitigated.
3. Default cookie names
Cookies help websites or web applications to identify a particular user since any user action on the web application gets stored as a cookie in the underlying infrastructure. Shopping carts in eCommerce websites are the most common examples of cookies. The cookies will remember the items you select on the website, and when you move to the checkout page, the shopping cart will display those items.
However, the problem with Node.js development arises when the developer opts for the default cookie names instead of customizing them as per the need. Since attackers are aware of the default cookie name, they are likely to attack and access the user input under the rich ecosystem effortlessly based on this information.
4. X-Powered-By header
X-Powered-By header is a common non-standard HTTP response header used by many scripting languages as a default option. With the help of server and configuration management techniques, you can either enable or disable this header. However, developers may fail to disable the X-Powered-By header, and that gives attackers access to some vital information. This header reveals the technology used in app development and permits attackers to exploit various security vulnerabilities associated with that particular technology.
5. Brute-force attacks
Brute force attacks are among the most recurrent attacks or risks you will find in any Node.js security checklist. The attackers generate random passwords and try implementing them on login endpoints of web applications to access critical information. Brute forcing is all about making millions of combinations until you find the correct password for the web application. To prevent brute-force attacks, you will need to strengthen your authentication mechanism for Node.js applications. Additionally, you can also limit the number of login attempts from one IP to deal with such risky situations and utilize bcrypt.js to safeguard the passwords stored in the database.
6. Distributed Denial of Service (DDoS) attacks
Limiting these attacks is extremely essential for ensuring the smooth performance of your Node.js applications as they can crash your servers, networks, or services and damage your rich ecosystem. Limiting the number of requests can significantly help in mitigating them.
7. Cross-Site Scripting (XSS) attack
An attacker can use XSS to send a malicious script to the end-user, and the end-users browsers have no way to identify the trustworthiness of the codebase. As a result, they execute it by default, and attackers can access any cookies, session tokens, or other sensitive information. These scripts can also rewrite the content of any HTML page, making XSS quite fatal.
Best practices for improving Node.js security
We had experiences in the past where clients were concerned if a particular backend framework like Node.js, is secure. Sadly, there isn’t a straightforward answer to this question as each technology can be hacked or exploited with the right resources. While the risks are tricky to anticipate, it is always preferable to follow the best practices.
1. Keep an eye on logging and monitoring to avoid irregularities
Irregular logging and monitoring can cause several security vulnerabilities that can cost the company a fortune. Most CTOs recommend frequent penetration tests that can help in detecting irregularities instead of waiting for an incident to be reported.
The French game development company, Voodoo, provides an excellent example of regular monitoring and logging activities. Memory leaks usually become a threat for an application that regularly receives large amounts of inputs from its users. The development team at Voodoo used Node.js API Inspector to collect metrics and real-time data of the application. By regularly monitoring KPIs, mainly CPU consumption, it was able to avoid memory leaks successfully.
Recommended modules to use for logging and monitoring activities:
- Node.js internal modules like Winston, Bunyan, and Pinto allow streaming. These can be used to handle uncaught exceptions and querying logs.
- The logs could also be used for feeding Intrusion Detection or Intrusion Prevention System (IP/IPS).
- Modules like toobusy-js can be used for monitoring the event loop.
- Monitoring modules also keep track of response time when the server gets busy or CPU consumption rises.
2. Make use of flat Promise chains to avoid layers of nesting
Asynchronous callbacks are considered to be one of the finest features of Node.js, compared to the previous basic callback functions. However, this feature can turn into one of the worst nightmares with increasing layers of nesting. For instance, if the nesting layers go beyond ten, it causes errors due to which results may get lost within the asynchronous callbacks.
What is the solution to the nesting problem?
- Use flat Promise chains to avoid callback hell.
- Flat Promise chains have the potential to control programming semantics.
- It can increase the flow of code by detecting errors and exceptions.
3. Avoid blocking the event loop for neat performance
While there seems to be no inherent danger to Node.js’s single-thread event-driven architecture, things get a little tricky when CPU-intensive JS operations are executed.
When a new client connection is established with the application, the EventLoop sends a response to the client. This is not just for new connections, but every incoming and outgoing request passes through the Event Loop. In any situation when the Event Loop gets blocks, the new and current clients will not get a chance to establish a connection with the application.
This phenomenon is known as “blocking” that causes a threat even when a regular expression is being resolved. A block in the event loop means greater chances of security vulnerabilities.
What can be done to avoid blocking the event loop?
- Node.js gives developers the option to assign callbacks to IO-blocked events, allowing the callback to run asynchronously.
- Operations relying on one another can be written in a single non-blocking function.
- Employing the below-mentioned command function that doesn’t block the event loop: (Source: OWASP)
4. Managing uncaught exception to avoid security loopholes
For an uncaught exception, Node.js usually prints the current stack trace and terminates the entire thread. Given that all uncaught exceptions are not necessarily security vulnerabilities, Node.js allows customization of the behavior through an EventEmitter object.
Unhandled code rejections usually lead to security loopholes. Improper handlers and resource allocation over unnecessary features can cause uncaught exceptions, directly making the application vulnerable to threats. In an effort to deal with all the uncaught exceptions, “triggerUncaughtException<” helps alert the developers and determine errors within their programming syntax in real-time.
How to leverage EventEmitter for managing uncaught exceptions?
- The EventEmitter object emits the uncaughtException to the main event loop. It is advisable to bind up the uncaughtException to the event and clear unallocated resources like handlers and file descriptors.
- Even the use of EventEmitter leaves a possibility of errors being left within the event chain, causing the application to crash unexpectedly. To avoid this, show a customized error message on display for the users instead of giving away the exact error.
5. Make strong authentication policies
Strong authentication policies are one of the Node.js best practices you should implement in your ecosystem to enhance security. An incomplete, broken, or weak authentication system is the root cause of security breaches. Therefore, as a Node.js developer, you should prioritize making high-level and robust authentication policies for your web application. Usually, developers consider their application to be well protected and in no need of any extra layer of security. However, when an attack occurs, they’re left with no options. That’s why security experts emphasize making stringent authentication policies from the beginning.
In January 2010, Google announced that the Chinese government was targeting Google to gain access to the email ids of users worldwide. This attack led to several changes in the Google security infrastructure and policies. For starters, it shut down operations in China and introduced two-factor authentication for business accounts in 2011. Thereafter, Google introduced a similar facility for the average user. Moving to solid authentication policies helped Google create a more robust and secure environment. It also enabled them to prevent any kind of security threats. After that, Microsoft, Twitter, Apple, and Amazon adopted two-factor authentication and are reaping its benefits.
Here’s how you can make strong authentication policies:
- Execute “multi-factor authentication” or “two-factor authentication” on your application login to get rid of weak passwords.
- Consider using ready-to-use authentication solutions like OAuth, Firebase Auth, or Okta.
- Use Scrypt or Bcrypt libraries instead of utilizing Node.js in-built crypto library.
- Work on ensuring high-quality session handling policies.
- Restrict failed login attempts and never tell the user whether the username or password is incorrect.
- Always prefer solutions that follow security standards and have necessary certifications.
6. Remove unnecessary routes
A web application should not have any page that ] users are not accessing anytime, anywhere. If the application contains pages that users rarely visit, it will increase the chances of attacks. So, it is essential to remove or disable all the unnecessary API routes in the Node.js application. For instance, Sails and Feathers, one of the top Node.js frameworks that automatically generate REST API endpoints.
In such a case, there’s every chance of information leakage as you won’t have complete control over your web application routes. So, to avoid such situations, you need to know the frameworks or libraries inside out and understand the routes they generate. You should also verify the mechanism to disable or remove these routes. One of the most popular cloud service providers— IBM Bluemix–offers users the option to remove unnecessary routes and ensure security for their web applications.
7. Handle errors to prevent unauthorized attacks
Handling errors are crucial for the steady functioning of an application and preventing unauthorized attacks. You must keep in mind that the application might display or leak sensitive information like stack traces to the client during an error. The moment attackers know the application’s vulnerability, they might send repeated requests causing the application to crash or a denial of service.
To avoid this, a real-time geospatial application like Uber sends constant notifications to both drivers and users for easy connection through matchmaking and road mapping. Uber realized that error handling in Node.js is as flawless as it could get with the aid of internal components and external tools. Apart from appropriate error analysis, the framework allowed fast code deployment, which secured the application from constant false requests.
What can be done to handle errors efficiently?
- While express routes are known for handling errors, it can even crash the app if an error is unhandled within an asynchronous callback.
- An Error object can be added in the first argument to an asynchronous call to ensure that errors are handled within the callback.
- Wrap express routes with a catch clause to avoid displaying or leaking sensitive information.
- Components like a load balancer, cloud firewall, or Ngnix can be used to limit DOS attacks.
8. Send only necessary information to avoid data leakage
Data leakage is one of the most common problems every Node.js developer needs to deal with. First, you need to ensure complete control over the information sent to the front-end. Most of the time, all the information is sent to the front-end for a specific object and then filtered out to display only restricted information. However, hackers these days can gain even access to the hidden data sent to users through the backend.
Byju’s, one of the most popular EdTech platforms, faced a significant data breach in June 2021. The platform was dependent on ‘Salesken.ai’ for Customer Service Management. It collects customers’ data like voice and chat logs, tracks customers’ behavior, and caters to their services and offers. Now, one of the servers of Salesken was unprotected, which became the point of contact for attackers, and 20 million student data were compromised.
Therefore, a system should only send the essential data. For example, if you want to display only first and last names, make sure you request the same. It needs you to write a detailed SQL statement and make a long database query, but it’s worth the effort as it will help you prevent data or information leaks.
Here are some of the ways through which you can avoid data leakage:
- Constantly evaluate the risk of using third-party services before implementing them. Ensure the services follow security compliances like HIPAA, PCI-DSS, or GDPR.
- Keep a keen eye on network security and track network access regularly.
- Identify the most sensitive data and enforce the strictest possible security measures.
- Use various encryption techniques to encrypt all the essential data.
9. Limit request size to avoid DOS attacks
A necessary security concern within Node.js is to ensure that the request size is limited to avoid large request bodies sent by attackers. It is important to remember that the bigger the payload (body size), the more difficult it is for the single thread to process the request.
As a result, attackers send large amounts of requests that can drain the server memory, crash the application, or even fill up the disk space, resulting in a Denial of Service.
Anticipating a sudden rise in the user base on the ONA Dating application, we utilized Node.js for improving scalability and performance. We knew that denial of service would be a massive disappointment for its users, and crucially, a security threat.
We took this seriously, and this is how we avoided DOS attacks:
- Limiting the request size by utilizing raw-body, external tools like firewall and ELB.
- Configuring express body-parser to accept small-size payloads.
- Requesting size limits for specific content types using express middleware and validate the data against the content type stated in the request headers.
10. Set up Cookie flags for session management
Session management is a significant aspect of web applications that helps maintain security and process multiple requests over the site. Any information related to session management over a web application is sent through cookies, and inappropriate use of HTTP cookies is definitely a recipe for vulnerabilities. It’s also important to check the scope of cookies through the domain attribute. The attribute tells if the domain matches against the domain server in which the URL is requested.
How do we avoid session vulnerabilities and security threats?
- Set up cookie flags like httpOnly, Secure, or SameSite.
- httpOnly flag can be used as a measure against XSS attacks.
- The ‘Secure’ flag ensures that cookies are sent only if the communication is over HTTP.
- SameSite flag secures the session from CSRF attacks.
11. Validate user inputs for restricting the XSS attacks
Apple faced a massive data and security breach in September 2021 when Israeli spyware infected iOS devices via a zero-click exploit. The spyware was injecting malicious scripts to the client-side. Through this exploit, it could record users’ calls, messages, and emails, and even turn on their cameras and microphones without their knowledge.
Here’s how you can restrict the Cross-Site Scripting attacks in Node.js:
- Make use of output encoding methods or tools such as XSS-filters or Validatorjs.
- Always try to escape the rendering of dynamic content.
- Implement the content-security policies in your browser.
- Make use of the HTML sanitization and generic libraries.
- Set the HttpOnly flag in your codebase.
- Scan and monitor your web applications regularly.
12. Ensuring secure deserialization to prevent CSRF attacks
Insecure deserialization is one of the most common Nodes.js security vulnerabilities that comprises deserialization of buggy and error-prone objects’ remote code execution or API calls. It commonly occurs under Cross-Site Request Forgery (CSRF) attacks. It forces users to take unnecessary and unwanted actions on the web applications. As discussed earlier, CSRF attacks change the state of an application and comprise sensitive data. Ensuring secure deserialization in Node.js applications would help you avoid CSRF attacks.
Twitter faced a significant security breach in July 2020. While investigating, the engineering team at Twitter found that it was due to social engineering carried through phishing attacks. The attackers gained access to the internal security system of Twitter and attacked 130 accounts, ultimately Tweeting from 45, accessing the DM inbox of 36, and downloading the Data of 7 users. So, if a major tech giant like Twitter is dealing with such issues, you must prioritize CSRF attacks.
Here are some other ways through which you can prevent CSRF attacks:
- Make use of anti-CSRF tokens in your Node.js applications.
- Try using the SameSite flag in the session cookies.
- Consider implementing user interaction-based protection on sensitive operations.
- Try to use custom request headers throughout the Node.js application.
- For stateful software, use the token synchronization pattern.
- For stateless software, use double submits cookies.
13. Execute access control on each request
If you want to examine the Node.js application security, you should understand the user permissions for various URL paths. Access control is one of the essential mechanisms to enforce stringent security measures on Node.js applications. For example, if you want a user not to access some part of the application, such as the admin dashboard, you can define user roles and enforce them through access control. However, by default, most Node.js applications don’t have an access control mechanism, so users can easily access any sensitive information.
Here’s how you can ensure access control on each request for Node.js applications:
- Manually test the app modules that require specific user permissions.
- Set up log access controlling and API rate restricting on the server.
- Take the assistance of middlewares to implement access control rules.
- Consider using role-based or attribute-based access control.
14. Use appropriate security headers
Security headers are a routine utilized by web applications to configure security defenses in web browsers. They help website owners to protect their websites from generalized web security vulnerabilities. There are many standard HTTP security headers that help you prevent Node.js web applications from vicious attacks. Let’s understand a few of them in detail:
- Strict-Transport-Security: With the help of HTTP Strict Transport Security (HSTS), you can force the browser to access the application only via HTTPS connections.
- X-XSS-Protection: It gives security against XSS attacks. You should set the header to 0 for enabling this protection.
- X-Content-Type-Options: This header tells browsers not to change the MIME types mentioned in the ‘Content-Type’ header.
- X-Frame-Options: The header determines whether you should load the web page through <iframe> or <frame> element and saves it from Clickjacking attacks.
- Content-Security-Policy: It helps prevent the XSS and Clickjacking attacks by allowing access to the content you decide by adding to the list.
- Cache-Control: This header prevents browsers from caching the responses. You should implement this header for web pages that contains sensitive information.
- X-Download-Options: The header prevents Internet Explorer from downloading files in the site’s content.
- Public-Key-Pins: This header increases the security of HTTPS. With the help of this header, you can associate a cryptographic public key with a particular server. If the server doesn’t use this key, the browser will consider responses illegal.
15. Make sure that packages are up to date
Using npm (Node Package Manager), you can manage all the dependencies. However, to ensure the latest security updates, we highly recommend that all third-party packages are kept up-to-date, irrespective of the framework in use. We might be swayed away by how efficient a third-party open-source package helps in the development process, but it must be kept in mind that these same packages are among the top OWASP vulnerabilities.
What practices should be mandated to ensure that packages are up to date?
- Be aware of the third-party applications used in the Node.js framework.
- Keep a tab on the security issues and developments from known bulletins.
- Use npm audit which warns you about all the vulnerable packages.
16. Avoid using dangerous functions to maintain application stability
It is noteworthy that specific functions and modules within Node.js are considered as ‘dangerous’ functions. These can seriously comprise the platform security of the application. The two typical dangerous functions that experts would warn against are the Eval function and the Chid Process function.
Apart from functions, modules may also pose a security threat. Some of these modules and functions include vm module, setTimeout function, execScript, setInterval function, and setImmediate function.
17. Use security linters and SAST for methodological testing
It is not possible to keep track of each and every security vulnerability. Hence, it is natural to overlook some that might arise out of a faulty code, component, or function. This is the very reason analytical testing methodologies are employed to scan and identify programming errors that might lead to a security breach.
How do we keep track and have a keen eye on vulnerabilities?
- The Static Analysis Security Testing can be utilized for the development and component testing process.
- SAST accelerates the development process and reduces the costs of modifying or updating applications in case of any issues in the future.
- Lint tools can automatically test code source code, identify faults, and alert the developer of the potential vulnerability within the app.
- Examples of JS linting tools used within Node.js include ESLint, JSHint, and TSLint.
It is quite common for companies to implement a centralized security testing and scanning facility for applications. Unfortunately, such centralized testing facilities do not provide much aid to the developers in identifying a threat in real-time while programming. To overcome this issue, microblogging sites like Twitter have adapted a self-service scanning by utilizing the SAST method and allowing developers to have complete control over their programming syntax.
Know about the best Node.js frameworks for web applications
18. Make fluid build pipelines to strengthen system security
When you leave the web application or servers unprotected or have weak security standards in place, security configuration vulnerabilities will take place. Due to this type of vulnerability, the whole app ecosystem, i.e., app containers, database, server, etc., are under security threat. The primary reason for these threats is weak build pipelines. These build pipelines are the entry points for security misconfiguration attacks. Therefore, by making the build pipeline fluid and strong, you can protect your Node.js web application from any type of security vulnerability.
Here’s how you can ensure fluid build pipelines for your Node.js applications:
- Keep every environment (development, staging, and production) equal with credentials and access levels.
- You should have customized package settings instead of using the default setting.
- Always change the default user password as it can cause brute force attacks.
- Establish complete control over the webserver and application security.
19. Run Node.js as a non-root user
There is a common scenario where you run the Node.js application as a root user with unlimited privileges. Unfortunately, if used incorrectly it can cause a threat to privacy and security. The reason is that the attackers who run any script on the server will get access to these unlimited privileges, which may compromise your sensitive information. That’s why experts recommend using Node.js as a non-root user. For example, when you use Docker for containerization, the default behavior is of the root user. But, to secure the Node.js application, it’s essential to run the process by invoking the container with the flag “-u username.”
20. Use strict mode
Top tools to utilize for enhanced Node.js security
Security threats and vulnerabilities are part and parcel of the frontend framework. Along with the above best practices for ensuring security with Node.js, it is wise to use tools to maintain extra security from attackers and detect existing vulnerabilities.
Here is a list of some of the tools that can be used to maintain Node.js application security.
Snyk is a developer-first cloud-native application security tool that has created a reputation for scanning and fixing bugs within any container, open-source libraries, or code. The best feature of this application is its real-time monitoring facility which alerts the developers to fix particular vulnerabilities. Along with its integration ability into GitHub, Circle CI, Code Ship, Jenkins, Tarvis, and Bamboo, it has its internal repository of updated vulnerability databases for quick detection of risks and threats.
Security of HTTP headers is often ignored by developers, and it has the potential to leak sensitive information to attackers. As a middleware, Helmet is a collection of 12 Node modules and, it follows the best OWASP practices for securing headers for increasing security in Node.js. The middleware can be used in Express and Koa, allowing you to implement HTTP response headers.
3. Source Clear
Keeping track of third-party packages, components, and modules can become a time-consuming activity, even though you are supposed to keep an updated record of it. Source Clear tools make the job easy by utilizing a “vulnerable methods identification” to identify if the vulnerable dependency is used within the application. With an extensive database of its own and constant feeding updates from recent bulletins, the tool can reduce false positives and provide detailed reports of the threats within the program.
Application security and testing are principal activities required to ensure that the application is rolling out entirely safe for its clients and users. Be it websites, web applications, or APIs, Acunetix scans the entire server-side of the application and gives actionable results. The tool is capable of scanning over 7000 vulnerabilities along with scanning multi-level forms and password-protected areas of the site.
If you are looking for open-source Node.js security testing tool, Retire.js is the perfect option as a command-line scanner. While third-party packages and components would inevitably be used, Retire.js scans known vulnerabilities within the codes and triggers a warning for the developer about its use. The tool also has plugin components and browser extensions that are regularly updated from various sources for providing precise security alerts.
Over the years, security vulnerabilities and threats have cost companies thousands of dollars. While breaches can make a big hole in the pocket, sensitive data leaks and compromised information cannot be priced in simple amounts. We might not be able to stop every attack that attackers might initiate to harm our apps, but we can ensure that our carelessness does not cause extensive damage.
With this article, the intention is not to explain simply the best practices one should follow while developing an application but also to consider security at every step software development lifecycle. This article is part of the checklist that our expert team of Node.js developers follows at Simform.
Subscribe to our blog to keep receiving updates on Node.js security or get in touch with our developing experts to expand your team and create a secure Node.js application.