DEV Community

Cover image for Enhance Security for NodeJS Applications
chauhoangminhnguyen
chauhoangminhnguyen

Posted on • Edited on • Originally published at howtodevez.blogspot.com

Enhance Security for NodeJS Applications

1. Limiting the number of requests πŸŽ€πŸŽ€πŸŽ€

Limiting the number of requests (from a single IP address within a specific timeframe) is a method used to prevent denial-of-service (DOS, DDOS) attacks or brute force attacks that could overload your server.

Limiting the number of requests

If you're using Express, integrating this is quite straightforward using the express-rate-limit package.



import * as express from 'express'
import helmet from 'helmet'
import expressRateLimit from 'express-rate-limit'

const app = express()
const limiter = expressRateLimit({
  windowMs: 10 * 60 * 1000, // ms, ~10 minutes
  max: 50, // limit each IP to 50 requests
})
const specificLimiter = expressRateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour window
  max: 2, // start blocking after 2 requests
  message: 'Too many requests', // default 429 TOO MANY REQUESTS
})

app
  .use(limiter) // use for all route
  .use('/common', (req, res) => {
    res.json({api: 'common', ok: true})
  })
  .get('/specific-api', specificLimiter, (req, res) => { // use middleware for specific route
    res.json({api: 'specific', ok: true})
  })
  .listen(3000)


Enter fullscreen mode Exit fullscreen mode

Note that if your development environment involves numerous test cases requiring requests to various APIs, it's advisable to exclude the rate limit. Always check the environment before starting the server to ensure that the rate limit is added in the production environment (or any desired environments).

2. Limiting the payload request size πŸŽ€πŸŽ€πŸŽ€

Limiting the payload size sent to the server helps prevent DOS/DDOS attacks. Fortunately, Express also provides us with this capability as follows:



import * as express from 'express'

const app = express()
app
  .use(express.json({limit: '50kb'})) // body-parser defaults to a body size limit of 100kb
  .post('/send-data', (req, res) => {
    if (!req.is('json')) {
      return res.sendStatus(400) // Bad request
    }
    res.json({ok: true})
  })
  .listen(3000)


Enter fullscreen mode Exit fullscreen mode

3. Using Helmet πŸ“―πŸ“―πŸ“―

Helmet is an npm package that includes middleware to handle and filter out malicious request headers (exploiting XSS vulnerabilities or clickjacking, for example). You can utilize Helmet's default configuration or customize it based on your needs following the instructions provided here.

Using Helmet

Using it is as simple as:



import * as express from 'express'
import helmet from 'helmet'

const app = express()
app
  .use(helmet())
  .listen(3000)


Enter fullscreen mode Exit fullscreen mode

4. Validate User Data Sent to the Server πŸ“―πŸ“―πŸ“―

Even if data validation is performed solely on the frontend, it's crucial to validate it on the backend as well. Frontend validation primarily serves regular users, while hackers may exploit vulnerabilities like SQL Injection to directly attack the server. Check out the following article for guidance on using express-validator to validate request data. It's a simple yet effective solution to enhance your server's security.

Validate User Data Sent to the Server

5. Use ORM Libraries (Object-Relational Mapping) ⭐⭐⭐

Utilize packages like TypeORM or Sequelize to mitigate serious security risks like SQL/NoSQL Injection. These packages offer convenient and secure functions suitable for connecting to multiple databases or migrating to different databases without significantly altering logic. While there are debates about the impact of using these packages on system performance, you can enhance performance through various methods. Moreover, considering the security risks, prioritizing this aspect is essential.

Avoid concatenating strings when querying to prevent hidden security risks:



const idValue = req.id; // get id from request so that attackers can attach malicious code
const query = `SELECT * FROM table_name a WHERE a.id = ${idValue};`;


Enter fullscreen mode Exit fullscreen mode

6. Avoid Storing Secret Info Directly in the Codebase ⭐⭐⭐

Sensitive information like secret keys, database passwords, API keys, and other crucial data should never be stored directly in the source code. Instead, place these details in a .env file (and remember to add the .env file to .gitignore). Here's a simple way to do it:

Install the dotenv package.



yarn add dotenv


Enter fullscreen mode Exit fullscreen mode

Creating a .env file



PASSWORD=password


Enter fullscreen mode Exit fullscreen mode

Accessing Value



process.env.PASSWORD


Enter fullscreen mode Exit fullscreen mode

Additionally, you can utilize Key Management Service (KMS) to store secret information on the cloud. Many providers like AWS, GCP, etc, offer support. Each method, whether using a .env file or KMS, has its pros and cons, so it's essential to weigh your options and choose what suits your needs best.

7. Choosing an Algorithm to Hash Passwords 🌱🌱🌱

Some hashing functions like SHA1, MD5, SHA-256 have been around for a while, but their hash/second rate is low, making them unsuitable for password hashing. Instead, opt for algorithms with a high hash/second rate like bcrypt or pbkdf2 to generate more complex hash strings. This will make it significantly more challenging and nearly impossible for attackers to crack passwords, as it will require a considerable amount of time and effort.

I'll provide a simple example using bcrypt to hash and compare passwords as follows:



import * as bcrypt from 'bcrypt'

const saltRounds = 10,
password1 = 'password1',
password2 = 'password2'

// 2 ways to gen hash
const hash1: string = await new Promise(rs =>
bcrypt.genSalt(saltRounds, (err, salt) => {
bcrypt.hash(password1, salt, (err, hash) => {
rs(hash) // store hash in your password DB.
})
})
)
const hash2 = await bcrypt.hash(password2, saltRounds)

// load hash from your password DB to compare
bcrypt.compare(password1, hash1, (err, result) => {
console.log(password1, hash1, result)
})
const result = await bcrypt.compare(password2, hash2)
console.log(password2, hash2, result)

Enter fullscreen mode Exit fullscreen mode



  1. Using HTTPS 🌱🌱🌱

When you use HTTP (Hypertext Transfer Protocol), data sent to the server isn't encrypted and can be intercepted and read by hackers. Nowadays, browsers like Chrome, Firefox, etc., display warnings when you access websites using HTTP. This can diminish your website's credibility to users and affect its ranking on search engines.

Using HTTPS (HTTP Secure) means that data sent to the server is encrypted, making it difficult for attackers to exploit sensitive information from those requests. However, it's important to note that accessing a website with HTTPS doesn't guarantee complete security.

Using HTTPS

To implement HTTPS for your website quickly and easily, you can use Cloudflare. It's a popular third-party service that acts as a proxy between users and your server, providing support for certificates and encryption. Simply sign up for a free Cloudflare account and configure details like your IP address and domain name. Since it's a third-party service, access speed may be slightly slower than usual.

Conclusion πŸ†πŸ†πŸ†

Through this article, you've learned some methods to enhance security for your NodeJS application. These methods are relatively straightforward and can be implemented right away in your current project.

Don't hesitate to leave your thoughts in the comments section, and remember to like, share, and follow for more insightful content in the future!πŸ™πŸ™

If you found this content helpful, please visit the original article on my blog to support the author and explore more interesting content.πŸ™πŸ™

BlogspotDev.toFacebookX


Some series you might find interesting:

Top comments (1)

Collapse
 
lirantal profile image
Liran Tal

Good list. I also teach a lot of secure coding in Node.js on my blog: nodejs-security.com/blog