DEV Community

Poorshad Shaddel
Poorshad Shaddel

Posted on • Originally published at levelup.gitconnected.com on

How to prevent SSRF attacks in Node.js

This articles explains what is SSRF attack, how usually attackers make it and how we can prevent it.

Why it is important to know SSRF?

By taking a look at HackerOne Top Ten Volunuriblities we can quickly understand the importance of SSRF and why me and you as developers should care about it.

A Table shows that SSRF is the fourth common bounty
HackerOne Bounty Table By Type

What is a SSRF Attack?

SSRF is the abbreviation for Server Side Request Forgery. Let’s explain it with an example instead of classic definitions. Imagine that you have a application, and your users need profile picture. Suddenly you decide that just uploading the picture is not enough for my users should be able to pass a URL, and I will download the picture and use it as their avatar. The problem begins in here. Downloading the picture means a GET request to a server that we do not have that much information about. Now that the attacker nows that you are making these GET requests he passes a URL like this to you: http://localhost/admin . If your system did not consider this attack you will send a GET request to your host admin which is definitely dangerous when someone can somehow interact with your Internal Systems or Services. Now if you are showing the result of the image download request it could make it much easier for attacker to explore your internal services and do something. I should mention that this type of attack is not limited to Internal Services and it could affect also External Systems.

The intention of the SSRF Attack is usually to exploit trust relationships to escalate an attack from the vulnerable application and perform unauthorized actions.

Different Types of SSRF Attacks

  • Server Attacks : In the example of downloading user Avatar from a URL if we pass something like localhost or 127.0.0.1 or the IP of the server we are attacking the server itself.
  • Internal Backend Systems : In this case attacker tries to communicate with other backend systems and for doing that he uses URL’s that contain private network IPs like http://192.168.0.68/admin or even using names like https://payment/something .
  • External Systems : This could be an external system that only your server or service is able to interact. This could be a bank API or anything that is restricted their service to your IP address with some other forms of authentication and authorization.


Schema of a SSRF attack in attempt to get access to 10.0.0.1

What is Basic SSRF and Blind SSRF?

In the example of downloading user picture from a URL if we return the response directly to the user, then the attacker is able to see the response and explore our services much faster. If we do not show any response from the request then the attack type becomes blind SSRF which becomes harder for the attacker to explore our systems without seeing the response.

An Example of a SSRF Attack

What This Service Is Doing : It is a resume builder website and when you are filling a field for your personal website it checks if it is a valid website or not and gives you a feedback to fix your URL if it cannot make a request to it! This application has a base part which has routes and you have to authenticate yourself to access these routes. We have a internal service called Payment that only the main app have access to it and it is not public.(This is full of simplification and the intention is to understand a situation that this could happen).

Let’s see the main app. It consists of :

  • personal-website: for checking the url and it gives you the message that says success and also returns response that came from your personal website(This is not a common thing to do but we are doing this to not make it a blind attack) to show a preview of the website to you.
  • me/balance: for checking current user’s balance. It makes a request to our payment service.
  • me/payin: for adding money to my wallet. It makes a request to our payment service.
const express = require('express');
const app = express();
const axios = require('axios');
app.use(express.json());
const paymentServiceAddress = 'localhost:3011'
app.post('/personal-website', async (req, res) => {
    const url = req.body?.url;
    try {
        const websiteHomePageTest = await axios.get(url);
        await saveAddressInProfile(url);
        res.json({ message: 'success', url, data: websiteHomePageTest.data });
    }
    catch (error) {
        if (axios.isAxiosError(error)) {
            return res.json({ message: 'failed', error: error.message }).status(500);
        } else {
            console.error(error);
            res.json({ message: 'failed' }).status(500);
        }
    }
});

app.post('/me/balance', someAuthenticationMiddleware, async (req, res) => {
    const response = await axios.post(`http://${paymentServiceAddress}/1/balance`);
    const balance = response.data.balance;
    res.json({ balance });
})

app.post('/me/balance/payin', someAuthenticationMiddleware, async (req, res) => {
    const payIn = await payIn()
    // now we want to update the balance
    const response = await axios.post(`http://${paymentServiceAddress}/1/balance/increase`);
    const balance = response.data.balance;
    res.json({ balance });
})

app.listen(3010, () => console.log(`Example app listening at http://localhost:3010`));
async function saveAddressInProfile(image) { console.log('save the image'); }
function someAuthenticationMiddleware(req, res, next) { next(); }
async function payIn() { }
Enter fullscreen mode Exit fullscreen mode

If we use a valid URL for our personal website we get this result:


HTTP Client that shows the message is success

At this point we are sure that this app is making a request.

Now if we try some other URL’s we can check other services that could be exploit.


Using local URL for SSRF

It gives us information about the server and internal services.

Now the attacker should start playing with the URL to find some other services. Finally if we try an address like http://localhost:3011 we see that the result becomes success and it means that there is a service behind this URL. Let’s see the implementation of payment service. We cannot make direct requests to this service since it is in a private network but we can make requests from the main app.

const express = require('express');
const app = express();
app.use(express.static('static'));
app.use(express.json());

app.get('/', (req, res) => {
    res.send('pong');
});

app.get('/:userId/balance', async (req, res) => {
    const balance = getUserBalance(req.params.userId);
    res.json({ balance });
});

const users = [
    { id: 1, name: 'John', balance: 100 },
    { id: 2, name: 'Jane', balance: 200 },
    { id: 3, name: 'Jack', balance: 300 }
]
function getUserBalance(userId) {
    return users.find(user => user.id == userId)?.balance;
}

app.listen(3011, () => {
    console.log(`Example app listening at http://localhost:3011`);
});
Enter fullscreen mode Exit fullscreen mode

After a little bit of trying the attacker could find that there is a route that gives balance to the user.(when the URL is not correct the payment service returns 404 and we see the result in the response).

By using the balance route which is a GET request we can access all users balance by using their userId. Normally by using this route on main app /me/balance we were able to see our own balance but now we have unauthorized access to other users’ balance too.


Unauthorized Access to Users’ Balance

How to Prevent SSRF Attacks

Now it is time to think about solution and prevent this attack step by step.

1- Validation

Black List

You can use Regex for validating the URL or having a black list of forbidden phrases like 127.0.0.1 or localhost. You can use regex directly or you can use validators like Zod ,hapi , validatorjs and so many other options.

IP Address

If you are accepting IP addresses or if you have a whitelist or black list that you want to check against, you can use this library for that: ip-address

Domain

If you are only accepting Domain names then this library could be helpful: is-valid-domain. It is archived but it does not have any dependency to other projects.

Warning

Be aware of the problem that there are so many ways to bypass the validation. For example:

  • Using an alternative IP representation of 127.0.0.1, such as 2130706433, 017700000001, or 127.1.
  • Registering your own domain name that resolves to 127.0.0.1. You can use spoofed.burpcollaborator.net for this purpose.
  • Obfuscating blocked strings using URL encoding or case variation.

So this input validation is just the beginning.

2- Network Layer

If a service does not interact with another service it should not have access to that service in internal private network. Since we are working with docker and k8s a lot this might happen that two pod or container do not have any relation but they can make requests to each other. This could be a potential issue. If a pod is using another pod called database and other pods do not need this database then these other pods should not be able to make requests to this pod(Azure Example).

3- Use SSRF Agents

By using Nodejs libraries like ssrf-req-filter or ssrf-agent you can prevent passing private URL’s to your app.

4- Use Proper Authentication and Authorization

In our example we saw that the main application had access to make any kind of requests to payment service. A better approach could be to pass in the user authentication information(it might be a JWT or session or anything) and then payment gets the userId from JWT payload and then we are sure that this user has access to this data. Even backend services should have limited access on each other. If they rely on user access then it could be harder to get unauthorized access.

5- Do Not Expose Response of Unknown Requests

Another mistake in the example was that it was exposing a http request response both in success and failure mode. Any extra information could be a potential clue for the attacker. So when the personal website is not available or gives internal server we should not care. We should always return something similar, for example a bad request.

Conclusion

Overall making a request based on client url is a risk and by doing validation, checking that it is not a private IP address or not and testing against some black list phrases we can reduce the risk. Best thing to do is to have a concrete authentication authorization system that could stay resilient even when the systems are making requests that they should’t.

Thanks for reading this article.

In case you like it you can check my previous articles about security in Nodejs:

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


Top comments (0)