Preventing SSRF in Express
Introduction

Welcome back! In the previous lesson, we explored the concept of Server-Side Request Forgery (SSRF) vulnerabilities and how to detect them. Now, we'll focus on preventing SSRF in Express applications. By the end of this lesson, you'll understand how to secure your Express applications against SSRF attacks, ensuring a safer web environment. Let's dive in! 🌟

Understanding SSRF in Express

Express is a popular web application framework for Node.js, known for its simplicity and flexibility in handling HTTP requests. However, this flexibility can sometimes lead to vulnerabilities if not handled properly. SSRF vulnerabilities in Express often arise when user input is not validated, allowing attackers to manipulate server-side requests.

When an Express application receives a request, it processes the input and may make further requests to external resources. If this input is not properly validated, an attacker can craft a request that tricks the server into making unintended requests, potentially accessing sensitive data or services.

The Vulnerable Code

Let's examine a piece of code that demonstrates how SSRF vulnerabilities can occur in an Express application:

import express from 'express';
import axios from 'axios';

const app = express();
app.use(express.json());

app.post('/fetch-url', async (req, res) => {
  const { url } = req.body;
  try {
    const response = await axios.get(url);
    res.json({ data: response.data });
  } catch (error: any) {
    res.status(500).json({ error: 'Failed to fetch URL' });
  }
});

In this code, the application accepts a URL from the user and fetches data from it using axios. However, there's no validation to ensure the URL is safe or trusted. This lack of validation can lead to SSRF vulnerabilities, as attackers can provide malicious URLs to exploit the server.

Exploiting the Vulnerability

An attacker can exploit this vulnerability by providing a malicious URL to the /fetch-url endpoint. Here's an example of how an attack might be performed:

# Attacker crafts a request to the vulnerable endpoint
curl -X POST http://localhost:3000/fetch-url -H "Content-Type: application/json" -d '{"url": "http://internal-service.local"}'

In this example, the attacker sends a POST request to the /fetch-url endpoint with a URL pointing to an internal service. If the application does not validate the URL, it may inadvertently access internal resources, leading to potential data breaches or unauthorized actions. This is especially dangerous in environments like cloud platforms (AWS, Azure, GCP), where internal endpoints such as metadata services are exposed only to internal IPs. By exploiting SSRF in such cases, an attacker can retrieve sensitive data like instance credentials or configuration tokens, which may then be used to escalate privileges or access cloud APIs directly.

Input Validation

To prevent SSRF attacks, it's crucial to implement robust input validation. Let's start by validating the user input to ensure it meets our security requirements:

app.post('/fetch-url', async (req, res) => {
  const { url } = req.body;

  // Validate URL
  if (!url || typeof url !== 'string') {
    return res.status(400).json({ error: 'Invalid URL' });
  }

  // ... rest of the code
});

In this step, we check if the URL is present and is a string. This basic validation helps ensure that the input is in the expected format before proceeding.

URL Whitelisting

Next, we'll implement URL whitelisting to restrict requests to trusted domains only:

import express from 'express';
import axios from 'axios';
import { URL } from 'url';

const app = express();
app.use(express.json());

// List of allowed domains
const ALLOWED_DOMAINS = ['trusted-domain.com', 'api.trusted-domain.com'];

// Example of SSRF prevention
app.post('/fetch-url', async (req, res) => {
  const { url } = req.body;

  // Basic validation
  if (!url || typeof url !== 'string') {
    return res.status(400).json({ error: 'Invalid URL format' });
  }

  try {
    // Parse the URL to validate its components
    const parsedUrl = new URL(url);
    
    // Ensure only HTTP/HTTPS protocols are used
    if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
      return res.status(400).json({ error: 'Only HTTP/HTTPS protocols are allowed' });
    }
    
    // Check if domain is in our whitelist
    const isDomainAllowed = ALLOWED_DOMAINS.some(domain => 
      parsedUrl.hostname === domain || parsedUrl.hostname.endsWith(`.${domain}`)
    );
    
    if (!isDomainAllowed) {
      return res.status(400).json({ error: 'Domain not allowed' });
    }

    // Proceed with the request to the validated URL
    const response = await axios.get(url);
    res.json({ data: response.data });
  } catch (error) {
    if (error instanceof TypeError) {
      return res.status(400).json({ error: 'Invalid URL' });
    }
    res.status(500).json({ error: 'Failed to fetch URL' });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

By checking if the domain is in our whitelist, we limit the scope of where requests can be made. This prevents attackers from directing the server to malicious or internal URLs.

Using URL Object for Validation

For more robust URL validation, we can use the Node.js URL object:

import { URL } from 'url';

// Validate URL using URL object
try {
  const parsedUrl = new URL(url);
  
  // Check protocol
  if (parsedUrl.protocol !== 'https:') {
    return res.status(400).json({ error: 'Only HTTPS is allowed' });
  }
  
  // Check hostname against whitelist
  const allowedDomains = ['trusted-domain.com', 'api.trusted-domain.com'];
  if (!allowedDomains.some(domain => parsedUrl.hostname === domain || parsedUrl.hostname.endsWith(`.${domain}`))) {
    return res.status(400).json({ error: 'Domain not allowed' });
  }
  
  // Now we can safely make the request
  const response = await axios.get(url);
  res.json({ data: response.data });
} catch (error) {
  if (error instanceof TypeError) {
    return res.status(400).json({ error: 'Invalid URL' });
  }
  res.status(500).json({ error: 'Failed to fetch URL' });
}

Using the URL object allows us to easily validate different parts of the URL, such as the protocol and hostname.

Implementing IP Address Restrictions

To further enhance security, we can block requests to internal IP addresses:

import ipaddr from 'ipaddr.js';

// Function to check if an IP address is private
function isPrivateIP(hostname: string): boolean {
  try {
    // Try to parse the hostname as an IP address
    const addr = ipaddr.parse(hostname);
    return addr.range() === 'private' || addr.range() === 'loopback';
  } catch (error) {
    // If parsing fails, it's not a valid IP address
    return false;
  }
}

// In your route handler
try {
  const parsedUrl = new URL(url);
  
  // Check if hostname is an IP address and if it's private
  if (isPrivateIP(parsedUrl.hostname)) {
    return res.status(400).json({ error: 'Private IP addresses not allowed' });
  }
  
  // Rest of your validation logic...
} catch (error) {
  // Handle errors...
}

This function uses the ipaddr.js library to check if the hostname is a private IP address, preventing requests to internal resources.

Conclusion

In this lesson, we explored how to prevent SSRF vulnerabilities in Express applications. We learned about the importance of input validation, URL whitelisting, and secure HTTP requests. By implementing these security measures, you can significantly reduce the risk of SSRF attacks in your applications.

In the next lesson, we'll dive deeper into monitoring and responding to SSRF incidents, helping you build a comprehensive security strategy for your Express applications.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal