Logo
blank Skip to main content

Securing Node.js Applications: Best Practices and Expert Tips

Securing your web solutions from vulnerabilities and being able to protect them against potential attacks is crucial for your projectโ€™s success and your businessโ€™s reputation. However, to do so, your team must be aware of dozens if not hundreds of nuances and potential challenges, including the specifics of the tools involved in the project.

There are a variety of efficient frameworks for web development, including React, Django, and Angular. Here, we solely focus on Node.js (as itโ€™s the most used web technology by professional developers according to the Stack Overflow 2024 Survey), discussing best practices and offering recommendations for delivering protected and reliable Node.js solutions.

In this post, we explore the importance of going the extra mile when protecting your Node.js solutions from vulnerabilities and attacks. Youโ€™ll find a variety of proven techniques enhanced with pro tips from Apriorit experts to help you secure your Node.js web application. This article will be helpful for product managers and development leaders who are interested in delivering protected software and are looking for relevant expertise.

The importance of Node.js application security

Node.js applications often handle sensitive information, including personal details, financial data, and proprietary business records. Also, Node.js solutions are particularly susceptible to various vulnerabilities due to their reliance on numerous third-party libraries from the node package manager (npm) ecosystem. Therefore, your team must pay extra attention to protecting your Node.js solutions from threats and securing all the data they store and handle.

Why enhance your Node.js software security

By ensuring robust protection of your Node.js project, you can:

  • Minimize the risks of unauthorized interference with the software, ensuring uninterrupted operation of your services and maintaining a positive customer experience.
  • Protect confidential information, preventing incidents with corporate secrets and user data.
  • Reduce the risk of application failure as a result of attacks or exploited vulnerabilities.
  • Save money and effort on handling the consequences of hacking, information leakage, and legal issues.
  • Meet regulatory compliance requirements your project is obliged to follow (like the GDPR or HIPAA).

With the importance of ensuring Node.js application security in mind, letโ€™s move to practices that can help your team protect your solution.

Have a Node.js project in mind?

Apriorit expert developers are ready to help you turn your ideas into a functioning and protected product!

5 best practices to secure a Node.js application

5 best practices to secure Node.js apps

1. Apply strict mode. Strict mode in JavaScript ensures more thorough code checking and helps with avoiding lots of common mistakes. It includes additional syntax and behavioral rules that help to improve code quality and detect potential issues.

In modern versions of Node.js, strict mode is enabled by default for modules implemented and launched in ECMAScript mode. However, to ensure compatibility with older versions of Node.js and to explicitly indicate strict mode, developers often add use strictย to the top of a file or function.

2. Use ORM/ODM libraries. Two important steps in making your app run smoothly and securely are enabling easy and efficient work with databases and preventing query vulnerabilities that make your app prone to SQL injections. To achieve both of those goals in your Node.js application, consider using Object-Relational Mapping (ORM) and Object-Document Mapping (ODM) libraries.

But to ensure a proper level of security, itโ€™s essential to know how to use these libraries correctly. Here are a few practice-based recommendations for preventing SQL query vulnerabilities when using ORMs and ODMs:

  • Instead of raw SQL queries, use methods and functions provided by the ORM/ODM to interact with databases.
  • Always use parameterized queries instead of string concatenation to avoid SQL injection.
  • Check and validate data before storing it in the database to prevent malicious content injection.
  • Regularly update libraries to benefit from the latest vulnerability fixes and security practices.
  • Use authentication and authorization mechanisms to ensure least privilege access to data and operations.

3. Store secrets securely. Encrypting stored secrets and using secret management systems ใƒผ like Kubernetes Secrets, Docker Secrets, and Vault ใƒผ is a critical aspect of software security.

Letโ€™s explore a few ways to keep secrets safe in Node.js applications:

  • Use environment variables. Node.js often stores configurations as environment variables. Thus, you can set API keys, passwords, and other secrets as environment variables in the operating system, or via a configuration file.
  • Leverage built-in tools. If your project already involves tools like Docker Secrets or Kubernetes Secrets, your team can also use built-in mechanisms for secret management. For example, Docker Secrets offers the possibility to store encrypted secrets and transmit them to containers.
  • Benefit from dedicated solutions for extra protection. If your project demands separate systems for safeguarding secrets, consider tools like Vault that provide centralized secrets management and protect access to stored secrets. In Node.js applications, you can also use libraries like node-vault to interact with the Vault API.

4. Use CORS. Cross-Origin Resource Sharing (CORS) is a mechanism that allows a web page to request resources from a domain other than the one it was loaded from. Without CORS, browsers enforce the Same-Origin Policy, which prohibits loading resources from other domains and limits resource exchange. CORS helps you prevent XSS and CSRF attacks while controlling access to server resources from different sources.

The cors middleware packet adds relevant CORS headers to all server responses. When a browser makes a request to a web resource, it includes the Origin header, indicating the source of the request (domain and protocol). A server then can check this header and allow or deny a request based on the CORS configuration.

5. Protect user accounts. In Aprioritโ€™s practice, we rely on two main methods for improving account security:

  • Enforcing the use of strong user passwords. Your application must require users to create passwords that contain various characters including uppercase and lowercase letters, digits, and special symbols. You should also use common password lists and dictionaries to prevent the use of weak passwords. Another crucial consideration is to never store passwords as plain text. Instead, use hashing with salt to protect user passwords from unauthorized access. Node.js offers libraries like argon2 for safe password hashing.
  • Implementing multi-factor authentication (MFA). A common MFA mechanism includes verification via SMS or email codes. Alternatively, you can implement authentication via a specialized application like Google Authenticator or enable authentication via a physical device like YubiKey.

Among various Node.js packets for authentication and password security, your team may consider:

  • bcryptjs ใƒผ a password hashing library that provides a simple way to hash passwords with salt to ensure secure storage of user passwords
  • passport ใƒผ a popular authentication package for Node.js that includes authentication mechanisms for different approaches like MFA
  • speakeasy ใƒผ a package for implementing two-factor authentication (2FA) in Node.js that supports various 2FA methods, including generating one-time passwords and using temporary session tokens

Our list of recommendations for securing Node.js applications could go on and on. However, we wonโ€™t discuss all of them in detail here, as some were already covered in our previous blog posts, including:

  • Using HTTP headers to reduce the risks of XSS attacks, clickjacking, and MIME sniffing. To add such headers to your Node.js application, your team needs to configure a popular npm packet called helmet.
  • Auditing vulnerabilities in npm dependencies to help your team identify and fix vulnerabilities before malicious actors abuse them. We recommend using tools like npm audit and snyk.
  • Applying JSON Web Tokens (JWT), an open standard that allows for securely passing information in JSON format between two parties. In Node.js projects, developers often use JWT for user authentication and authorization.

Leveraging security best practices is good, but knowing what attacks to secure Node.js apps from (and how) is even better. To comprehensively protect your Node.js project, your team must create a solid strategy that combines a list of techniques and practices targeting specific attack types. More on this below.

Read also

Integrating Node.js Applications with Cloud Services [Explanations & Practical Examples]

Learn how to integrate your Node.js app into the cloud for maximum scalability and performance. Explore best practices for seamless deployment.

Learn more
Integrate-node-js-application-with-Cloud-provider-services-cover

4 common Node.js attacks and ways to prevent them

Below, we offer some insights from Aprioritโ€™s experience that can be used as a starting point in creating comprehensive anti-attack strategies.

Protecting Node.js web applications from 4 common attacks

Prevent brute-force attacks

To avoid brute-force attacks in Node.js projects, we recommend using a combination of techniques and packages, including the following:

1. Limit the number of login attempts. Set a limit on the number of login attempts per user over a certain period of time. If the number of attempts exceeds the set limit, make sure the app blocks access for the user for a specified period of time.

2. Enable CAPTCHA. A CAPTCHA requires the user to enter characters from an image, making automated brute-forcing more difficult. Implementing CAPTCHA mechanisms like Googleโ€™s reCAPTCHA for login and registration pages is a basic but reliable technique for improving an appโ€™s protection.

3. Use an express-bouncer. This middleware for Express.js automatically blocks IP addresses that launch too many unsuccessful login attempts, significantly helping in preventing brute-force attacks. Letโ€™s explore an example of using the express-bouncer middleware:

JavaScript
// Creating a new express-bouncer
const bouncer = require ("express-bouncer")(500, 900000); 
  
// Adding its address to the allow-list (optional) 
bouncer.whitelist.push ("127.0.0.1"); 
  
// Adding a custom error text (optional) 
bouncer.blocked = function (req, res, next, remaining) 
{ 
	res.send (429, "Too many requests, " + 
    	"please wait  " + remaining / 1000 + " seconds"); 
}; 
  
// Specifying a route we want to protect using express-bouncer 
app.post ("/login", bouncer.block, function (req, res) 
{ 
	if (LoginFailed) 
	{ 
    	// Login error  
	} 
  
	else 
	{ 
    	bouncer.reset (req); 
    	// Login successful 
	} 
});

4. Use express-brute. This is another piece of middleware that offers protection against brute-force attacks with flexible settings for IP address blocking and unblocking. Letโ€™s explore an example of using express-brute:

JavaScript
const express = require('express'); 
const ExpressBrute = require('express-brute'); 
const store = new ExpressBrute.MemoryStore(); // Itโ€™s possible to use other storage
const bruteforce = new ExpressBrute(store); 
  
const app = express(); 
  
app.post('/login', bruteforce.prevent, (req, res) => { 
  // Code for login and password checking 
  if (/* a condition for login failure */) { 
	return res.status(401).send('Invalid credentials'); 
  } 
  
  // Successful authentication 
  res.send('Login successful'); 
}); 
  
app.listen(3000, () => { 
  console.log('Server runs on port 3000'); 
});

Note that the choice between express-bouncer and express-brute will depend on your project requirements and preferences. Both packages provide effective tools for mitigating brute-force attacks in Node.js applications.

Prevent DDoS attacks

To handle the risk of DDoS attacks, make sure your team implements mechanisms for rate limiting. This can help you prevent attacks that are based on a large volume of requests from one source. However, the way you implement rate limiting mechanisms will depend on your projectโ€™s size. Letโ€™s explore two examples:

1. For large Node.js projects, developers often use the Nginx web server as a proxy server before Node.js, as it allows them to limit requests by speed, bandwidth, and size.

Example of how to limit requests by speed:

JavaScript
http {  
	limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
	server { 
    	location / { 
        	limit_req zone=one burst=5; 
    	} 
	} 
}

Example of how to limit requests with low bandwidth:

JavaScript
http { 
	server { 
    	location / { 
        	include /etc/nginx/conf.d/slow.conf; 
    	} 
	} 
} 

Example of how to limit requests by size:

JavaScript
http { 
	server { 
    	client_max_body_size 10m; 
	} 
} 

We also recommend using Nginx modules and extensions like ngx_http_limit_conn_module and ngx_http_limit_req_module for protecting from DDoS attacks. They can significantly enhance the protection of web applications by limiting the number of simultaneous connections from a single IP address and limiting the rate of requests that can be processed by a server from a single IP address.

2. For small and medium-sized applications that donโ€™t use Nginx, consider using middleware and libraries like express-rate-limit that can help limit the number of requests from a single IP address for a certain period of time:

JavaScript
const express = require('express'); 
const rateLimit = require('express-rate-limit'); 
const app = express(); 
  
const limiter = rateLimit({ 
  windowMs: 15 * 60 * 1000, // 15 ะผะธะฝัƒั‚ 
  max: 100, // Specify maximum number of requests
  message: 'Request limit exceeded. Please try again later.', 
}); 
app.use('/api/', limiter);  // Apply restriction only to routes starting with /api/

Related project

Developing and Enhancing a Custom SaaS Platform for HR Management

Discover how Aprioritโ€™s team built a scalable HR management SaaS platform, optimizing processes and enhancing the user experience for smoother workforce management.

Project details
Developing a Custom SaaS Platform for HR Management

Prevent CSRF attacks

To mitigate Cross-Site Request Forgery (CSRF) attacks, use CSRF tokens to help your servers verify a requestโ€™s authenticity and check whether it was sent from a valid and expected source.

Hereโ€™s how it works:

  1. During form page loading (for example, ‘/secure-page’), the csurf middleware generates a unique CSRF token and sets it in a cookie.
  2. Then, the token is passed to a template and included in a hidden field as _csrf.
  3. During form submission, the application compares the _csrf value to the value stored in the cookie. If the values โ€‹โ€‹donโ€™t match, the request is considered invalid and the server may reject it, specifying a 403 forbidden error.

To start using CSRF tokens, your team first needs to install csurf packets for CSRF token generation and validation, and cookie-parser for working with cookies:

JavaScript
npm install csurf cookie-parser

Hereโ€™s an example of integrating csurf in a web application:

JavaScript
const express = require('express'); 
const cookieParser = require('cookie-parser'); 
const csrf = require('csurf'); 
  
const app = express(); 
  
app.use(cookieParser()); 
  
// Using csurf middleware to create CSRF tokens 
const csrfProtection = csrf({ cookie: true }); 
  
// Applying middleware to required routes
app.get('/secure-page', csrfProtection, (req, res) => { 
  // Generating and passing a CSRF token
  res.render('secure-page', { csrfToken: req.csrfToken() }); 
}); 
  
// Processing POST-requests and CSRF checking
app.post('/process', csrfProtection, (req, res) => { 
  // Checking a CSRF token
  if (req.csrfToken() !== req.body._csrf) { 
	return res.status(403).send('Invalid CSRF token'); 
  } 
  
  // The logic behind POST-request processing 
  res.send('POST request processed successfully'); 
}); 
  
app.listen(3000, () => { 
  console.log('Server is running on port 3000'); 
});

Prevent XSS attacks

One way to prevent Cross-Site Scripting (XSS) attacks is by using the escaping data process. Your team can protect user data that is included in HTML or CSS strings by creating special functions. Such functions ensure that any user input is treated as plain text rather than executable code.

Hereโ€™s an example of an escaping data function for HTML:

JavaScript
const escapeHtml = (unsafe) => { 
  return String(unsafe) 
    .replace(/&/g, '&') 
    .replace(/</g, '<') 
    .replace(/>/g, '>') 
    .replace(/"/g, '"') 
    .replace(/'/g, '''); 
}; 
  
const userProvidedText = '<script>alert("XSS attack!")</script>'; 
const sanitizedText = escapeHtml(userProvidedText);

And hereโ€™s an example of an escaping data function for CSS:

JavaScript
const escapeCss = (unsafe) => { 
  return String(unsafe) 
	.replace(/'/g, '\\\'') 
	.replace(/"/g, '\\\"'); 
}; 
  
const userProvidedStyle = 'background: url("javascript:alert(\'XSS\')");'; 
const sanitizedStyle = escapeCss(userProvidedStyle); 

We also recommend using the following Node.js libraries to mitigate XSS attacks: he for HTML and cssesc for CSS.
Example of using the he library for escaping data in HTML:

JavaScript
const he = require('he');
const userProvidedText = '<script>alert("XSS attack!")</script>';
const sanitizedText = he.encode(userProvidedText);

Example of using the cssesc library for escaping data in CSS:

JavaScript
const cssesc = require('cssesc');
const userProvidedStyle = 'background: url("javascript:alert(\'XSS\')");'; const sanitizedStyle = cssesc(userProvidedStyle);

Apart from high-level cybersecurity recommendations and comprehensive approaches, itโ€™s important that your software engineers also take into consideration secure coding practices. Using proven tips and tricks during all project stages will help your team ensure protection, resilience, and reliability of your future solution in the long run.

Read also

TypeScript vs JavaScript: Whatโ€™s the Difference?

Compare TypeScript and JavaScript to see which best fits your web solution. Discover their strengths and weaknesses for real-world projects.

Learn more
blog-207-article.jpg

5 tips and tricks for safeguarding Node.js apps [+ practical examples]

Each practice we offer below is a small but essential brick in the wall that secures your Node.js application from various threats and vulnerabilities.

5 tips for securing Node.js web applications

1. Type-check uploaded files

Attackers can use file uploading functionality to try injecting malicious code that will be executed when files are processed.

Proactive measures: Make sure your application always checks the type of an uploaded file. For Node.js applications, we recommend using the file-type npm package, which will mitigate malicious code execution by checking the file type first. Even if attackers try uploading a file with an extension that doesnโ€™t match its contents, this measure will check the file type authenticity based on its contents. Thus, an app can deny a file upload if the file type in invalid, significantly reducing the risk of malicious code injection.

Example:

JavaScript
const fileType = require('file-type');
console.log(await fileTypeFromFile('Unicorn.png')); // {ext: 'png', mime: 'image/png'}

2. Forbid using the eval function

The reason for this is that eval executes the code (thatโ€™s been passed to it) as a string, which can lead to unpleasant consequences like malicious code injection.

Proactive measures: To forbid using the eval function in code in Node.js applications, your team can use linting and code analysis tools like ESlint. Such tools can help your team detect and prevent the use of eval functions in your appโ€™s code.

Example: You can configure linter rules in the package.json or .eslintrc.json file. Hereโ€™s an example of configuring ESlint:

JavaScript
{ 
  "rules": { 
	"no-eval": "error" 
  } 
}

Pro tip: If you still need to allow your web application to use the eval function but want to prevent execution of dynamic scripts, consider applying Content Security Policy (CSP) headers to limit sources from which an app can execute scripts.

3. Prevent buffer overflow

Some malicious actors might want to affect your application by using attacks that overflow your buffer.

Proactive measures: Consider limiting the size of requests. For example, you can limit the data volume passed via the HTTP-request body in Node.js settings.

Example:

JavaScript
app.use( 
   express.json({ 
 	limit: '50mb', 
   }) 
);

Pro tip: If your application loads files, we recommend using middleware like mutler to limit the size of uploaded files. Hereโ€™s an example of using mutler to limit the file size:

JavaScript
const express = require('express'); 
const multer = require('multer'); 
  
const app = express(); 
const upload = multer({ limits: { fileSize: 1000000 } }); // File size limit up to 1 MB 
  
// Path for file uploading
app.post('/upload', upload.single('file'), (req, res) => { 
  // Processing the uploaded file
}); 
  
app.listen(3000, () => { 
  console.log('Server is running on port 3000'); 
});

4. Validate input data

If your app doesnโ€™t validate data that goes into input fields (numerical values, email addresses, etc.), it exposes itself to a number of potential issues including executing unwanted commands, which can potentially compromise the entire system. Invalid data can also cause unexpected behavior, leading to application crashes or slow performance. Such instability can frustrate users and degrade their experience.

Proactive measures: To identify and check input data, consider using Joi or Validator packets:

  • Joi offers a wide range of methods for defining schemas, checking data types, making sure required fields are present, and ensuring other validation rules.
  • Validator provides various functions for checking string values like emails, URLs, and numerical values.

Your choice between Joi and Validator will depend on your project needs. Joi offers more powerful features and a rich syntax thatโ€™s helpful for defining complex validation schemes, while Validator provides a lightweight and simple interface for basic string validation.

Example of using Joi:

JavaScript
const Joi = require('joi'); 
  
const schema = Joi.object({ 
  username: Joi.string().min(3).max(30).required(), 
  email: Joi.string().email().required(), 
  password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(), 
}); 
  
// Example of data validation 
const dataToValidate = { 
  username: 'john_doe', 
  email: '[email protected]', 
  password: 'securePassword123', 
}; 
  
const validationResult = schema.validate(dataToValidate); 
if (validationResult.error) { 
  console.error(validationResult.error.details); 
} else { 
  console.log('Data is valid'); 
} 

Example of using Validator:

JavaScript
const validator = require('validator'); 
  
// Example of email validation 
const emailToValidate = '[email protected]'; 
if (validator.isEmail(emailToValidate)) { 
  console.log('Email is valid!'); 
} else { 
  console.error('Email is invalid!'); 
}

5. Check access rights

Users must not have more access rights than they need to do their work. Ensuring this is essential for protecting data and managing the risk of insider threats.

Proactive measures: One of the key practices for distinguishing roles and user access rights is using the access control list (ACL). ACL is an access control mechanism that allows for determining which users or roles have the right to perform certain actions. To implement an ACL in your Node.js application, your team can use third-party libraries like acl or accesscontrol.

Example of using the acl library:

JavaScript
const express = require('express'); 
const acl = require('acl'); 
  
const app = express(); 
const port = 3000; 
  
// Initializing and configuring an ACL 
const aclInstance = new acl(new acl.memoryBackend()); 
  
// Adding roles and access rights
aclInstance.allow([ 
  { 
	roles: 'admin', 
	allows: [ 
  	{ resources: '/api/admin', permissions: ['get', 'post', 'put', 'delete'] }, 
	], 
  }, 
  { 
	roles: 'user', 
	allows: [ 
  	{ resources: '/api/user', permissions: ['get', 'post'] }, 
	], 
  }, 
]); 
  
// Middleware for checking access
const checkAccess = (req, res, next) => { 
  const userRole = req.headers['x-user-role']; // let's assume the role is passed in the header 
  
  aclInstance.isAllowed(userRole, req.path, req.method.toLowerCase(), (err, allowed) => { 
	if (err) { 
  	return res.status(500).json({ error: 'Access check error' }); 
	} 
  
	if (!allowed) { 
  	return res.status(403).json({ error: 'No rights to perform the operation' }); 
	} 
  
	next(); 
  }); 
}; 
  
// Routes that are protected by ACL 
app.get('/api/admin', checkAccess, (req, res) => { 
  res.json({ message: 'A request to an admin resource is made' }); 
}); 
  
app.get('/api/user', checkAccess, (req, res) => { 
  res.json({ message: 'A request to a user resource is made' }); 
}); 
  
// Launching the server 
app.listen(port, () => { 
  console.log(`Server runs on the port ${port}`); 
}); 

Conclusion

Node.js is a viable choice for building web applications, as this platform offers a wide range of tools for innovative and high-performance solutions.

However, ensuring high-level security of Node.js applications can be challenging. Your team needs to put effort into preventing cyber attacks, mitigating potential vulnerabilities, and safeguarding the overall application. This requires not only deep knowledge of Node.js tips and tricks but also vast experience creating different solutions, learning unexpected pitfalls, and finding ways to overcome them.

If you need assistance with implementing the practices and recommendations covered in this article, we are here for you. Our expert Node.js development teams know how to secure Node.js applications. We have rich experience delivering reliable and protected solutions for clients across different industries. And with our focus on cybersecurity, we pay attention to even the smallest security details.

Need help protecting your Node.js application?

Leverage Aprioritโ€™s cybersecurity expertise to bring your productโ€™s reliability and security to the next level!

Have a question?

Ask our expert!

Maryna-Prudka
Maryna Prudka

R&D Delivery Manager

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.