5 Common Server Vulnerabilities with Node.js
We recently published an article about OWASP Juice Shop and got a lot of great feedback from the community, so we decided to dive a bit deeper into some security-related topics.
In this article, we’ll discuss some of the common server vulnerabilities and offer some tips on what you can do to mitigate them.
Introduction
Node.js is a powerful and widely-used JavaScript runtime environment for building server-side applications. However, like any other software, Node is susceptible to vulnerabilities that can lead to security issues if you don’t properly address them.
Please do note that these vulnerabilities are not unique to Node, they can be found in every backend programming language.
This article will explore 5 common vulnerabilities:
- Injection Attacks
- Cross-Site Scripting (XSS)
- Denial-of-Service (DoS)
- Improper Authentication and Authorization
- Insecure Direct Object References
1. Injection Vulnerabilities
If not properly handled, applications are vulnerable to injection attacks, such as SQL injection, NoSQL injection, and Command Injection.
These types of attacks occur when an attacker inputs malicious code into a vulnerable application and the application executes it. You might think of input bar, for example.
An injection vulnerability might be a SQL injection, when untrusted data is concatenated into a SQL query. An attacker can inject malicious code into the query, which can then be executed by the database.
Take a look at the following code. It is susceptible to SQL injection (unfortunately).
const express = require("express");
const app = express();
const mysql = require("mysql");
const connection = mysql.createConnection({
host: "localhost",
user: "root",
password: "password",
database: "test",
});
app.get("/user", (req, res) => {
const id = req.query.id;
const query = `SELECT * FROM users WHERE id = ${id}`;
connection.query(query, (error, results) => {
if (error) {
throw error;
}
res.send(results);
});
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
As you might have noticed, the id
parameter from the query string is directly concatenated into the SQL query. If an attacker were to pass a malicious value for id
, such as 1 OR 1=1
, the resulting query would be SELECT * FROM users WHERE id = 1 OR 1=1
, which would return all records from the users
table. Yikes!
To prevent this type of vulnerability, it’s important to validate user input and use parameterized queries when working with databases. In the example above, this could be done by using a prepared statement and binding the id
value to the query, like this:
app.get("/user", (req, res) => {
const id = req.query.id;
const query = "SELECT * FROM users WHERE id = ?";
connection.query(query, [id], (error, results) => {
if (error) {
throw error;
}
res.send(results);
});
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
2. Cross-Site Scripting (XSS) Vulnerabilities
XSS attacks allow attackers to inject malicious scripts into web pages viewed by other users. This can result in sensitive information being stolen, such as login credentials or other sensitive data. To prevent XSS attacks, it’s important to sanitize all user-generated data and validate it before sending it to the client. To see an example in action, we wrote about OWASP Juice Shop which guides you through an XSS attack. Nonetheless, let’s explore some vulnerable code.
Here’s an example of vulnerable code that is susceptible to XSS attacks:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
const name = req.query.name;
res.send(`<h1>Hello, ${name}</h1>`);
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
The name
parameter from the query string is directly included in the HTML response. If an attacker were to pass a malicious value for name
, such as <script>alert('XSS')</script>
, the resulting HTML would include the attacker’s malicious script.
If you’d like to try it out, create a folder called xss
. Move to the folder and type npm init -y
and then npm i express
. Create a file called index.js
and paste the code above. After you run the file (node index.js
), navigate to your browser and visit localhost:3000
. To see the XSS attack in action, simply add the code you’d like to the query, like so:
localhost:3000/?name=<script>alert('XSS')</script>
You should see an alert message:
Okay, so what gives? Well, in this case, the URL is localhost
. There’s no major threat here, and nobody is in any danger. However, you can imagine for a second that an attacker has discovered one of these attacks on a major website. Say, a banking website, for example. Well, the URL can then become https://mysuperfakebank.com/?username=username&password=password&success=<script>'Enter script that emails bad guy credentials here'</script>
.
To prevent this type of vulnerability, we could use a library such as escape-html
.
const express = require("express");
const app = express();
const escapeHtml = require("escape-html");
app.get("/", (req, res) => {
const name = escapeHtml(req.query.name);
res.send(`<h1>Hello, ${name}</h1>`);
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
If you test the query again, you’ll see a different result:
3. Denial-of-Service (DoS) Vulnerabilities
DoS attacks are designed to overload the server and cause it to crash. This can be done through a variety of methods, such as sending a large number of requests to the server or flooding the server with data. This can cause companies to lose a lot of money ($20,000 per hour in the event of a successful attack).
To prevent DoS attacks, it’s important to implement rate-limiting, use proper error handling, and have a robust infrastructure in place.
Here’s an example of some vulnerable code that is susceptible to DoS attacks:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
// Do a resource-intensive operation
while (true) {}
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
In the code above, the server is susceptible to DoS attacks because it is not properly handling incoming requests. If an attacker were to send a large number of request to the endpoint, the server would become unresponsive as it tries to execute the infinite loop.
To prevent this type of vulnerability, it’s important to properly handle and validate incoming requests and to limit the amount of resources that a single request can consume. In the example above, this could be done by using a middleware to limit the maximum number of requests. We can use a nice package to handle this for us, express-rate-limit
and use it like so:
const express = require("express");
const app = express();
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: "Too many requests, please try again later",
});
app.use(limiter);
app.get("/", (req, res) => {
res.send("Hello, World!");
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
4. Improper Authentication and Authorization
Improper authentication and authorization can result in unauthorized access to sensitive data, which can lead to theft or damage. To prevent this, it’s important to implement proper authentication and authorization methods, such as using secure passwords and two-factor authentication.
Here’s an example of code that is susceptible to improper authentication:
const express = require("express");
const app = express();
app.get("/secret", (req, res) => {
res.send("This is a secret page!");
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
In this example, the /secret
endpoint is not properly protected, and anyone who knows the URL can access it.
To prevent this type of vulnerability, it’s important to properly implement and enforce authentication mechanisms. In the example above, we could accomplish this like so –
const express = require("express");
const app = express();
const checkAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).send("Unauthorized");
}
next();
};
app.get("/secret", checkAuth, (req, res) => {
res.send("This is a secret page!");
});
app.listen(3000, () => {
console.log("Example app listening on port 3000!");
});
The checkAuth
middleware is used to check if the user is authenticated before accessing the /secret
endpoint. If the user is not authenticated, the middleware will return a 401 Unauthorized
response.
5. Insecure Direct Object References
Just like improper authorization, in insecure direct object references, an attacker can access and manipulate objects directly, bypassing the intended security controls. Here’s an example of such vulnerability in Node.js:
const express = require("express");
const app = express();
const users = [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" },
];
app.get("/user/:id", function (req, res) {
let user = users.find((user) => user.id == req.params.id);
if (!user) {
res.status(404).send("User not found");
return;
}
res.send(user);
});
app.listen(3000);
Now, the code retrieves a user from the users
array based on the id
parameter passed in the URL (for example, /user/1
). This is a classic example of insecure direct object references as an attacker could potentially manipulate the id
parameter in the URL to access other users' data. To mitigate this vulnerability, the code should check that the user being retrieved is authorized to be accessed by the current user.
Conclusion
In conclusion, Node.js is a powerful and widely-used technology, but it’s important to be aware of potential vulnerabilities (as with all code you craft).
Feel free to run the code snippets on your machine and experiment with them.
Happy Hacking!
Recommended Resources
- Becoming a Hacker: Must Read Security & Cyber Crime Books
- Security Resources for Web Developers
- OWASP Juice Shop: Hacking a Web Application
- The Web Application Hacker’s Handbook
- The Tangled Web: A Guide to Securing Modern Web Applications
- Intro to Cybersecurity
- JavaScript Security (Coursera)
Full Disclosure: this post contains affiliate links. However, we only recommend books or products (courses) that we have personally read or used. We may receive a (very) small commission if you purchase any of the books or courses from this list (at no additional cost to you).
Related Posts
The Great JavaScript Debate: To Semicolon or Not?
Since I’ve started learning this language, JavaScript has undergone some heavy changes. Most notably, it seems to be the norm to not use semicolons anymore.
Read moreHacktoberfest 2024: Get a Free JavaScript Today Sticker
October is here, and that means one thing in the tech world—Hacktoberfest! This annual event, powered by DigitalOcean, Cloudflare, Quira, and other sponsors, encourages developers of all skill levels to contribute to open-source projects.
Read moreCreating a Real Time Chat Application with React, Node, and TailwindCSS
In this tutorial, we will show you how to build a real-time chat application using React and Vite,as well as a simple Node backend.
Read more