24 Oct 2023 · Software Engineering

    Best Practices for Securing Node.js Applications in Production

    14 min read
    Contents

    Node.js is one of the favorite technologies for developers when it comes to backend development. Its popularity keeps rising and is now one of the main targets of online attacks. That is why it is crucial to secure Node.js from vulnerabilities and threats.

    In this guide, you will see the 15 best practices for devising a secure Node.js app architecture for production. Implement them all to make your backend more secure than ever!

    Why Should You Build a Secure Node.js App?

    Building a secure Node.js application is important for at least three good reasons:

    1. Protecting User Data: Your application may handle sensitive user information such as personal details, login credentials, payment data, or confidential business insights. Failing to protect this data can cost you millions in fines from privacy regulators. By implementing robust security measures, you can safeguard user data and avoid legal issues.
    2. Safeguarding Application Functionality: Security vulnerabilities can compromise the features offered by your backend. Attackers may exploit weaknesses to alter your services, manipulate data, or inject malicious code. By securing your app, you can avoid that and provide a seamless experience for your users.
    3. Preserving Reputation: A security incident can seriously damage your reputation and erode trust in your service. Customers and users expect their data to be handled securely and your application to work as intended. A breach can lead to a loss of trust, but by prioritizing security you show your commitment to quality.

    You might think that security problems are not that widespread and it is enough to follow coding and architectural best practices, but this is not true. The power of Node.js lies in the NPM environment, which offers millions of libraries. The problem is that most NPM packages involve some security vulnerabilities.

    In other words, your Node.js project is not safe if its dependencies are not. Considering how popular it is to use external libraries, this is worrisome. According to the Snyk State of Open Source Security report, an average Node.js project has 49 vulnerabilities out of 79 direct dependencies. That concerning statistic underscores how important it is to protect your Node.js application.

    15 Best Practices to Make Your Node.js App More Secure

    Let’s take a look at the most popular best practices to secure your Node.js project.

    1. Never Run Node.js With Root Privileges

    Running Node.js with root privileges is not recommended as it goes against the principle of least privilege. No matter if your backend is on a dedicated server or Docker container, you should always launch it as a non-root user,

    If you instead run Node.js with root privileges, any vulnerabilities in your project or its dependencies can potentially be exploited to gain unauthorized access to your system. For example, an attacker could harness them to execute arbitrary code, access sensitive files, or even take control of the entire machine. Thus, using root users for Node.js must be avoided.

    The best practice here is to create a dedicated user for running Node.js. This user should have only the permissions required to launch the app. This way, attackers who succeed in compromising your backend will be restricted to that user’s privileges, limiting the potential damage they can cause.

    2. Keep Your NPM Libraries Up To Date

    NPM libraries make it easier and quicker to build a full-featured Node.js backend. At the same time, they can also introduce security risks into your application. New vulnerabilities are discovered all the time, and it is the maintainers’ job to address them and release an updated version of the package. Here is why you should keep your dependencies up to date.

    To ensure the NPM libraries you are relying on are secure, you can use npm audit and snyk. These tools analyze your project’s dependencies tree and provide insights into any known vulnerabilities.

    Here’s an example of running npm audit:

    npm audit
    ...
    found 4 vulnerabilities (2 low, 2 moderate)
    run `npm audit fix` to fix them, or `npm audit` for details

    This command leverages the GitHub Advisory Database and checks your package.json and package-lock.json against known security issues.

    Additionally, you can use Snyk to check your dependencies against Snyk’s Open Source Vulnerability Database. Install snyk with:

    npm install -g snyk

    In the root folder of your project, test your application with:

    snyk test

    To open a wizard that will walk you through the process of patching the vulnerabilities found, run:

    snyk wizard

    Using snyk and regularly running npm audit helps you identify and fix security issues in your NPM libraries before they might become a problem. Remember that the security of your app is only as strong as the weakest link in your dependencies.

    The cookie names used by your Node.js application can unintentionally reveal the technology stack your backend is based on. That is valuable information that you should always obscure, as attackers can use it again you. By knowing what framework you are using, they can exploit specific weaknesses associated with it.

    In detail, attackers tend to focus on the name of the session cookie. Protect your app from that by setting a custom session cookie name with the express-session middleware:

    const express = require('express');
    const session = require('express-session');
    
    const app = express();
    
    app.use(session({
      // set a custom name for the session cookie
      name: 'myCustomCookieName', 
      // a secure secret key for session encryption
      secret: 'mySecretKey', 
    }));

    4. Set the Security HTTP Headers

    The default HTTP headers in Express are not very secure. We check header security with the online Security Headers project:

    securing nodejs

    Some of these headers contain information that should not be publicly exposed, such as X-Powered-By. Others are missing and should be added to deal with various security-related aspects, including preventing cross-site scripting (XSS) attacks.

    Here is where helmet comes into play! This library takes care of setting the most important security headers based on the recommendations from Security Headers. Use it as follows:

    const express = require('express');
    const helmet = require('helmet');
    
    const app = express();
    
    // register the helmet middleware
    // to set the security headers
    app.use(helmet());

    The helmet() middleware automatically removes unsafe headers and adds new ones, including X-XSS-ProtectionX-Content-Type-OptionsStrict-Transport-Security, and X-Frame-Options. These enforce best practices and help protect your application from common attacks.

    The headers set by your Node.js app will now be considered secure:

    5. Implement Rate Limiting

    DDoS (Distributed Denial of Service) and brute force are two of the most common web attacks. To mitigate them, you can implement rate limiting. This technique involves controlling the incoming traffic to your Node.js backend, preventing malicious actors from overwhelming your server with too many requests.

    The easiest way to implement rate limiting is through the rate-limiter-flexible library. This dependency provides a configurable middleware to restrict the number of requests coming from the same IP address or user within a specified time frame.

    Here is an example of how to use it to apply rate limiting in Node.js:

    const express = require('express');
    const { RateLimiterMemory } = require('rate-limiter-flexible');
    
    const app = express();
    
    const rateLimiter = new RateLimiterMemory({
      points: 10, // maximum number of requests allowed
      duration: 1, // time frame in seconds
    });
    
    const rateLimiterMiddleware = (req, res, next) => {
       rateLimiter.consume(req.ip)
          .then(() => {
              // request allowed, 
              // proceed with handling the request
              next();
          })
          .catch(() => {
              // request limit exceeded, 
              // respond with an appropriate error message
              res.status(429).send('Too Many Requests');
          });
       };
    
    app.use(rateLimiterMiddleware);

    First, a rate limiter instance allowing a maximum of 10 requests in 1 second is initialized. Then, it is used in a custom middleware to analyze the IP of the incoming request. If the rate limit is not exceeded, the request proceeds. Otherwise, the request gets blocked and the server returns a 429 Too Many Requests response.

    6. Ensure Strong Authentication Policies

    To protect your Node.js application against attacks that exploit user authentication, you need to enforce strong authentication policies. First, invite users to set strong passwords. Also, you should support Multi-Factor Authentication (MFA) and Single Sign-On(SSO). MFA adds an extra layer of security by requiring users to provide multiple forms of authentication, while SSO simplifies the authentication process and reduces the risk of weak or reused passwords.

    When it comes to hashing passwords for storage, prefer strong cryptographic functions like bcrypt over the methods offered by the Node.js crypto library. That package provides a secure password-hashing algorithm that makes it significantly harder for attackers to crack passwords. Finally, mitigate brute-force attacks by restricting the number of failed login attempts through rate limiting as explained earlier.

    7. Do Not Send Unnecessary Info

    Any information provided to the attacker unintentionally can be used against you. For this reason, server responses should contain only what the caller strictly needs. For example, avoid returning detailed error messages or stack traces directly to the client. Instead, provide generic error messages that do not reveal specific implementation details. The easiest at to do so is to run Node.js in production mode setting the NODE_ENV=production env, otherwise Express will add the stack trace in the error responses.

    Similarly, you must be careful about the data included in API responses. Return only necessary data fields and avoid exposing sensitive information not requested by the caller. This will minimize the risk of accidentally disclosing confidential or privileged information.

    8. Monitor Your Backend

    Your backend in production may be under attack, and you may not even be aware of it. Here is why it is essential to monitor your Node.js application. By connecting it to an Application Performance Monitoring (APM) tool, you can keep track of it to identify security issues and ensure its overall health.

    Fortunately, there are several APM libraries and services available for Node.js. Some of the most popular are SigNozSentryPrometheusNew Relic, and Elastic These provide information on various aspects of the application, including performance, error rates, resource usage, and security-related metrics. In particular, they enable real-time data collection and detection of anomalies or suspicious activity that could indicate a security breach. Some of them also offer osservability features to also track the deployment workflow in your CI/CD pipeline.

    9. Adopt an HTTPS-Only Policy

    By ensuring that your backend is accessible only via HTTPS, you will improve the confidentiality of data exchanged between clients and your Node.js server. HTTPS establishes an encrypted channel that safeguards sensitive information like passwords, session tokens, and user data from interception.

    As part of such policy, you should also use HTTPS cookies. To achieve that, make sure that any cookies set by your Node.js application are marked as secure and httpOnly`:

    res.cookie('myCookie', 'cookieValue', {
      // create an HTTPS cookie
      secure: true,
      httpOnly: true,
    });

    Unintended parties or scripts will no longer be able to access your cookies. Plus, they will be transmitted exclusively over HTTPS connections.

    10. Validate User Input

    Whenever users have the opportunity to enter inputs, attackers can exploit that to send malicious data to the server. Therefore, validating user input is fundamental for ensuring the security and integrity of a Node.js application.

    Thanks to libraries like express-validator, you can enforce strict validation rules on the body and query parameters of incoming requests. Only data in the expected format will be allowed to pass through, avoiding unexpected behavior due to unforeseen input.

    11. Use Security Linters

    Security linters analyze your codebase to identify vulnerabilities, unsafe code sections, and best practice violations. One of the most popular is eslint-plugin-security, a set of ESLint rules to enforce security development in Node.js.

    By integrating such tools into your development workflow, you can spot and address security issues early on. Specifically, they reduce the risk of introducing vulnerabilities into your application while coding. These tools particularly effective when added integrated in the CI/CD pipeline.

    12. Prevent SQL Injection

    SQL injection is a common security vulnerability that occurs when an attacker can manipulate input data passed into an SQL query. This generally happens when concatenating user input directly into SQL queries. In that scenario, attackers can forge specific inputs aimed at executing arbitrary SQL code, leading to unauthorized access and data breaches.

    There are several ways to prevent SQL injection in Node.js. The most popular ones are:

    • Use Prepared Statements or Parameterized Queries: These techniques involve separating the SQL code from the user input, preventing it from being interpreted as part of the query.
    • Input Sanitization: Validate user input to reject malicious data, reducing the risk of SQL injection attacks.
    • Use an ORM: ORM technologies like Sequelize generally provide built-in protection against SQL injection.

    13. Limit Request Size

    The default request body size limit in Node.js is 5 MB. To protect your backend from DDoS attacks where malicious users try to flood your server with data, it is recommended to reduce that limit. To do so, you can use the body-parser library and configure it as below:

    const express = require('express');
    const bodyParser = require('body-parser');
    
    const app = express();
    
    // set the request size limit to 1 MB
    app.use(bodyParser.json({ limit: '1mb' }));

    Adjust the value according to your specific requirements. Now, requests with a body larger than 1 MB will now be blocked immediately, preventing the server from allocating resources to process them.

    14. Detect Vulnerabilities Through Automated Tools

    Automated vulnerability scanning tools, such as SonarQube or similar, are valuable resources for identifying security problems in Node.js applications. These tools perform comprehensive scans of the codebase, dependencies, configurations, and other components to identify security weaknesses.

    Here are some key benefits of using automated vulnerability scanners:

    • Early Detection: Proactively identify security issues before deploying your application.
    • Increased Coverage: Perform in-depth scans of all project files, ensuring high security coverage.
    • Continuous Monitoring: Integrate them into your CI/CD pipeline to ensure that any new vulnerabilities introduced through code changes are promptly discovered.

    15. Make It Easy to Report Vulnerabilities

    Giving users and security researchers the ability to report vulnerabilities found in your Node.js backend is another important aspect to keep your app secure. Not only should this be possible, but the procedure needs to be clear and accessible.

    An effective approach to allow researchers to reach out to you is the security.txt proposed standard. This is a simple text file placed at the root of your project that provides information on how to report security vulnerabilities. It follows a standardized format and includes contact details, encryption methods, and disclosure guidelines.

    Here’s an example of a basic security.txt file:

    Contact: email@example.com
    Encryption: https://example.com/pgp-key.asc

    Contact specifies the email address security vulnerabilities or concerns should be reported to. These emails may contain critical information and should not be publicly accessible. So, the Encryption field indicates the location of the organization’s PGP public key that can be used for encrypting messages inside the e-mails. This mechanism ensures that only the organization can decrypt those messages with the private key and read them.

    Similarly, you could also consider adding a “Report Vulnerability” page on your website.

    Conclusion

    In this guide, we discussed ways of securing Node.js applications in production. As developers, it is our duty to protect user data, maintain application functionality, and preserving our reputations.

    By integrating these techniques, approaches, and tips, you can create a more reliable Node.js architecture, mitigating risks and ensuring the security of your application.

    3 thoughts on “Best Practices for Securing Node.js Applications in Production

    1. This is one of the best article I came across. Thanks for sharing, It covers essential security aspects of the application.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.
    Avatar
    Reviewed by:
    I picked up most of my skills during the years I worked at IBM. Was a DBA, developer, and cloud engineer for a time. After that, I went into freelancing, where I found the passion for writing. Now, I'm a full-time writer at Semaphore.