As a popular runtime environment for building web applications, Node.js has become a Go-to choice for many developers. However, with great power comes great responsibility, and it's important to consider the security implications of using Node.js in your projects. Node.js in 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.
Top 5 security risks of Node.js
1. CROSS-SITE SCRIPTING (XSS)
2. CROSS-SITE REQUEST FORGERY (CSRF)
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application
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
A distributed denial-of-service (DDoS) attack is a malicious attempt to disrupt the normal traffic of a targeted server, service or network by overwhelming the target or its surrounding infrastructure with a flood of Internet traffic.
DDoS attacks achieve effectiveness by utilizing multiple compromised computer systems as sources of attack traffic. Exploited machines can include computers and other networked resources such as IoT devices.
From a high level, a DDoS attack is like an unexpected traffic jam clogging up the highway, preventing regular traffic from arriving at its destination.
5. REGULAR EXPRESSION DENIAL OF SERVICE ATTACKS (REDOS)
The Regular expression Denial of Service (ReDoS) is a Denial of Service attack, that exploits the fact that most Regular Expression implementations may reach extreme situations that cause them to work very slowly (exponentially related to input size). An attacker can then cause a program using a Regular Expression (Regex) to enter these extreme situations and then hang for a very long time.
Top 6 best practices to keep Node.js secure
1. SETUP LOGGING AND MONITORING
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, warning, and debug. To cut down on manual effort, you can leverage modules like Bunyan and toobusy-js to automate logging and monitoring or you can use ELK Stack to fully monitor, view insights, and user journey.
2. ENSURE YOU HAVE STRONG AUTHENTICATION POLICIES IN PLACE
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.
3. VALIDATE USER INPUT TO LIMIT SQL INJECTIONS ATTACKS
Let’s take a code example
The above query is SQL injection vulnerable. Why? Because the id parameter is taken directly from the front end. Instead of sending just the id, the attacker can manipulate the request and send SQL commands with it. Instead of sending just 1521 (the id of the book), the attacker can send 1521; DROP TABLE BOOKS, and Node.js will wipe your database.
The basic idea is to not blindly pass parameters from the front end to the database query. Instead, you need to validate or escape values provided by the user. How to do it exactly depends on the database you use and the way you prefer to do it. Some database libraries for Node.js perform escaping automatically (for example node-MySQL and mongoose). But you can also use more generic libraries like Sequelize or knex.
4. Avoid errors that reveal too much
There are a few things to consider here. First, don’t let the user know the details, i.e., don’t return the full error object to the client. It can contain information that you don’t want to expose, such as paths, another library in use, or perhaps even secrets. Second, wrap routes with the catch clause and don’t let Node.js crash when the error was triggered from a request. This prevents attackers from finding malicious requests that will crash your application and sending them over and over again, making your application crash constantly.
Speaking of flooding your Node.js app with malicious requests, don’t directly expose your Node.js app to the Internet. Use some component in front of it, such as a load balancer, a cloud firewall or gateway, or old good nginx. This will allow you to rate limit DoS attacks one step before they hit your Node.js app.
5. AVOID SECRETS IN CONFIG FILES
Writing secure code from the beginning will definitely help, but it won’t make your application bulletproof if you end up storing plain text secrets in your config files. This practice is unacceptable even if you store the code in a private repository. Importing secrets from environment variables is the first step, but it’s not a perfect solution either. To be more confident that your secrets aren’t easily readable, use secret management solutions like Vault. Whenever using Vault isn’t possible, encrypt your secrets when you store them, and be sure to rotate them on a regular basis. Many CI/CD solutions allow you to securely store secrets and securely deploy them.
6. IMPLEMENT HTTP RESPONSE HEADERS & DON’T RUN NODE.JS AS ROOT
Many less common attacks can be avoided by adding additional security-related HTTP headers to your application. The most basic mechanisms like CORS will improve the security of your API, but consider using modules like helmet, which will add even more headers in order to secure your application. Helmet can implement eleven different header-based security mechanisms for you with one line of code: app.use(helmet()); If you want to know the details, here’s the full list.
Securing web applications is important, but tight deadlines sometimes prevent us from properly executing at any given stage. That’s why it’s important to consider security at every step of the software development lifecycle, from conception all the way to production.