What SSRF is and how to protect your web app against this type of attack


SSRF occurs when an application allows users to supply a URL or other request parameters, which the server then uses to make HTTP or other network requests. An attacker manipulates these inputs to force the server to send requests to resources that the attacker shouldn’t have access to, such as:
1. Internal Resources:
• Internal services like databases (http://127.0.0.1:3306).
• Internal APIs, admin panels, or other services running on private IP ranges (10.x.x.x, 192.168.x.x, etc.).
• Cloud metadata endpoints (http://169.254.169.254/latest/meta-data), exposing sensitive information like AWS credentials.
2. External Resources:
• Malicious websites controlled by the attacker to exfiltrate sensitive data.
• Services vulnerable to Denial of Service (DoS) attacks.
Now lets look at a vulnerable code and see how we can remediate it.
To remediate SSRF (Server-Side Request Forgery) in a Node.js and Express application, you need to validate user inputs, restrict the server’s ability to make unauthorized network requests, and implement security best practices.
Here’s how you can secure your application step-by-step:
- Vulnerable Code Example
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.use(express.json());
// Vulnerable endpoint: directly fetches user-supplied URLs
app.post('/fetch-data', async (req, res) => {
const { url } = req.body;
if (!url) {
return res.status(400).send('URL is required');
}
try {
const response = await fetch(url);
const data = await response.text();
res.status(200).send(data);
} catch (err) {
res.status(500).send('Failed to fetch the URL');
}
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
- Securing the Code
Here’s a secure version of the same endpoint, incorporating multiple layers of protection:
const express = require('express');
const fetch = require('node-fetch');
const { URL } = require('url'); // Built-in Node.js module for URL validation
const app = express();
app.use(express.json());
// Define trusted domains (whitelist)
const ALLOWED_DOMAINS = ['example.com', 'trusted.com'];
// Function to validate URLs
function validateUrl(inputUrl) {
try {
const parsedUrl = new URL(inputUrl);
// Check if the protocol is HTTP or HTTPS
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
throw new Error('Invalid protocol');
}
// Check if the hostname is in the allowed list
if (!ALLOWED_DOMAINS.includes(parsedUrl.hostname)) {
throw new Error('Domain not allowed');
}
return parsedUrl.href;
} catch (err) {
return null;
}
}
// Secure endpoint
app.post('/fetch-data', async (req, res) => {
const { url } = req.body;
if (!url) {
return res.status(400).send('URL is required');
}
// Validate the URL
const validatedUrl = validateUrl(url);
if (!validatedUrl) {
return res.status(400).send('Invalid or disallowed URL');
}
try {
// Fetch the validated URL
const response = await fetch(validatedUrl, {
timeout: 5000, // Set a timeout to prevent hanging requests
});
const data = await response.text();
res.status(200).send(data);
} catch (err) {
console.error('Error fetching data:', err);
res.status(500).send('Failed to fetch the URL');
}
});
app.listen(3000, () => console.log('Secure server running on http://localhost:3000'));
Key Security Measures in the Code
1. Input Validation:
• Use the URL class to parse and validate user-supplied URLs.
• Restrict allowed protocols to http and https.
• Implement an allowlist (ALLOWED_DOMAINS) for trusted domains.
2. Timeouts:
• Set a timeout for fetch requests to prevent the server from hanging indefinitely.
3. Reject Internal IP Addresses (Optional): Add additional checks to block internal IP ranges such as 127.0.0.1 or 10.0.0.0/8.
const isInternalIp = (hostname) => /^(127\.|10\.|192\.168\.)/.test(hostname);
if (isInternalIp(parsedUrl.hostname)) {
throw new Error('Access to internal IPs is not allowed');
}
4. Error Handling:
• Gracefully handle errors from fetch (e.g., network errors, invalid URLs).
• Avoid exposing sensitive server information in error messages.
5. Logging and Monitoring:
• Log suspicious or blocked requests for auditing and debugging purposes.
console.warn(`Blocked request to: ${url}`);
Enhancing Security with Additional Tools
• Network-Level Restrictions:
• Use firewalls or cloud-based security rules (e.g., AWS Security Groups) to block requests to sensitive internal services.
• Rate Limiting:
• Prevent abuse by limiting the number of requests per user.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
});
app.use(limiter);
• Use Third-Party Libraries:
• Libraries like safe-url-input can simplify URL validation.
- Example Testing Scenarios
To test the security of this endpoint: 1. Allowed Domain:
curl -X POST http://localhost:3000/fetch-data
-H "Content-Type: application/json"
-d '{"url":"http://example.com"}'
2. Disallowed Domain:
curl -X POST http://localhost:3000/fetch-data
-H "Content-Type: application/json"
-d '{"url":"http://malicious.com"}'
3. Internal Resource Access (should be blocked):
curl -X POST http://localhost:3000/fetch-data
-H "Content-Type: application/json"
-d '{"url":"http://127.0.0.1:3306"}'
By implementing these safeguards, your Node.js and Express application will be well-protected against SSRF attacks while maintaining functionality for legitimate users.
Subscribe to my newsletter
Read articles from Hooman Pegahmehr directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Hooman Pegahmehr
Hooman Pegahmehr
Hooman Pegahmehr is a performance-driven, analytical, and strategic Technology Management Professional, employing information technology best practices to manage software and web development lifecycle in alignment with client requirements. He builds high-quality, scalable, and reliable software, systems, and architecture while ensuring secure technology service delivery as well as transcending barriers between technology, creativity, and business, aligning each to capture the highest potential of organization resources and technology investments. He offers 8+ years of transferable experience in creating scalable web applications and platforms using JavaScript software stack, including MongoDB, Express, React, and Node, coupled with a focus on back-end development, data wrangling, API design, security, and testing. He utilizes a visionary perspective and innovative mindset to collect and translate technical requirements into functionalities within the application while writing codes and producing production-ready systems for thousands of users. He designs, develops, and maintains fully functioning platforms using modern web-based technologies, including MERN Stack (MongoDB, Express, React, Node). As a dynamic and process-focused IT professional, Hooman leverages cutting-edge technologies to cultivate differentiated solutions and achieve competitive advantages while supporting new systems development lifecycle. He excels in creating in-house solutions, replacing and modernizing legacy systems, and eliminating outsourcing costs. He exhibits verifiable success in building highly responsive full-stack applications and incident management systems using advanced analytical dashboards while translating complex concepts in a simplified manner. Through dedication towards promoting a culture of collaboration, Hooman empowers and motivates diverse personnel to achieve technology-focused business objectives while administering coaching, training, and development initiatives to elevate personnel performance and achieve team synergy. He earned a winning reputation for transforming, revitalizing, streamlining, and optimizing multiple programs and web-based applications to drive consistent communications across cross-functional organization-wide departments. He manages multiple projects from concept to execution, utilizing prioritization and time management capabilities to complete deliverables on time, under budget, and in alignment with requirements.