Back to Blog
critical SEVERITY9 min read

How Server-Side Request Forgery (SSRF) Happens in Node.js fetch Tools and How to Fix It

A critical Server-Side Request Forgery (SSRF) vulnerability in `plugins/tools/fetch.js` allowed attackers to access internal resources and cloud metadata endpoints by passing arbitrary URLs to the fetch command. The fix adds hostname resolution and private IP range validation before executing any HTTP requests, preventing attackers from targeting internal infrastructure.

O
By Orbis AppSec
Published August 28, 2026Reviewed August 28, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in Node.js that occurs when user-supplied URLs are passed to fetch() without validation. An attacker could access internal resources like AWS metadata endpoints (169.254.169.254) or private IP ranges. The fix adds DNS resolution of the hostname and validates that the resolved IP address is not in private ranges (10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, and IPv6 equivalents) before allowing the request.

Vulnerability at a Glance

cweCWE-918
fixResolve hostname to IP address and validate against private IP ranges before making HTTP request
riskAttackers can access internal resources, cloud metadata endpoints, and exfiltrate sensitive data through the server
languageJavaScript (Node.js)
root causeUser-supplied URL parameter passed directly to fetch() without validation of target IP address ranges
vulnerabilityServer-Side Request Forgery (SSRF)

How Server-Side Request Forgery (SSRF) Happens in Node.js fetch Tools and How to Fix It

Introduction

In the plugins/tools/fetch.js file, a critical vulnerability allowed attackers to abuse the fetch command to access internal resources and sensitive cloud metadata. The vulnerable code at line 17 accepted a user-supplied URL via the text parameter and passed it directly to fetch(text) without any validation of the target IP address. This meant an attacker could craft a WhatsApp message with .fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/ to retrieve AWS instance credentials from the metadata service—a classic Server-Side Request Forgery (SSRF) attack.

The issue wasn't that the code validated URL format (it did check for https:// or http:// at line 16), but rather that it failed to validate where those URLs actually pointed. An attacker could use a perfectly valid URL format pointing to a private IP address, and the server would dutifully fetch it.

The Vulnerability Explained

The Vulnerable Code Pattern

module.exports = {
   help: ['fetch'],
   use: '<URL>',
   async run({ usedPrefix, command, text }) {
      if (!/^https?:\/\//.test(text)) throw Func.example(usedPrefix, command, 'https://google.com')

      const res = await fetch(text)  // ← VULNERABLE: No IP validation!
      const length = Number(res.headers.get('content-length') || 0)
      if (length > 100 * 1024 * 1024) throw `Content is too large: ${length} bytes`

The problem is subtle but critical: the code validates the URL format (lines 15-16) but never validates the URL destination. The regex check if (!/^https?:\/\//.test(text)) only ensures the URL starts with http:// or https://—it says nothing about where that URL points.

Why This Is Exploitable

An attacker can craft URLs that pass the format check but point to internal resources:

  • https://169.254.169.254/latest/meta-data/iam/security-credentials/ — AWS metadata endpoint
  • https://192.168.1.1/admin/ — Internal router admin panel
  • https://10.0.0.5:5432/ — Internal database service
  • https://127.0.0.1:8080/internal-api/ — Localhost-bound services
  • https://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity — Google Cloud metadata

All of these URLs pass the regex check but represent internal resources that should never be accessible via a user-controlled fetch command.

Exploitation Scenario

An attacker sends a WhatsApp message to a bot running this code:

.fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/

The server:
1. Checks the URL format ✓ (passes regex)
2. Calls fetch(text) ✗ (no IP validation)
3. Makes an HTTP request to the AWS metadata endpoint
4. Returns AWS credentials to the attacker

The attacker now has valid AWS credentials and can access all resources the instance has permission to access.

Real-World Impact

This vulnerability is particularly dangerous because:
- Cloud environments: Most modern applications run on AWS, GCP, or Azure where metadata endpoints expose sensitive credentials
- Internal services: Private databases, admin panels, and internal APIs become directly accessible
- Data exfiltration: Attackers can read files from internal file servers
- Lateral movement: Compromised credentials enable further attacks within the infrastructure

The Fix

The fix adds three critical security controls:

  1. Hostname resolution — Convert the URL's hostname to an IP address
  2. Private IP detection — Check if the resolved IP is in private/reserved ranges
  3. Explicit blocking — Reject requests to private IPs before calling fetch()

Before and After Comparison

Before (Vulnerable):

const util = require('util')

module.exports = {
   help: ['fetch'],
   use: '<URL>',
   async run({ usedPrefix, command, text }) {
      if (!/^https?:\/\//.test(text)) throw Func.example(usedPrefix, command, 'https://google.com')

      const res = await fetch(text)  // ← Directly fetches without IP validation
      const length = Number(res.headers.get('content-length') || 0)
      if (length > 100 * 1024 * 1024) throw `Content is too large: ${length} bytes`

After (Fixed):

const util = require('util')
const dns = require('dns').promises        // ← Added: DNS resolution
const net = require('net')                 // ← Added: IP validation utilities

function isPrivateIP(ip) {                 // ← Added: Private IP detection
   if (net.isIPv4(ip)) {
      const [a, b] = ip.split('.').map(Number)
      return a === 10 || a === 127 || a === 0 ||
         (a === 169 && b === 254) ||       // ← AWS metadata range
         (a === 172 && b >= 16 && b <= 31) ||  // ← Private range
         (a === 192 && b === 168)          // ← Private range
   }
   return ip === '::1' || /^fe80:/i.test(ip) || /^f[cd]/i.test(ip)  // ← IPv6 checks
}

module.exports = {
   help: ['fetch'],
   use: '<URL>',
   async run({ usedPrefix, command, text }) {
      if (!/^https?:\/\//.test(text)) throw Func.example(usedPrefix, command, 'https://google.com')

      const { hostname } = new URL(text)           // ← Extract hostname from URL
      const { address } = await dns.lookup(hostname)  // ← Resolve to IP address
      if (isPrivateIP(address)) throw 'This URL is not allowed.'  // ← Block private IPs

      const res = await fetch(text)        // ← Now safe to fetch
      const length = Number(res.headers.get('content-length') || 0)
      if (length > 100 * 1024 * 1024) throw `Content is too large: ${length} bytes`

How Each Change Works

  1. const dns = require('dns').promises — Imports Node.js's DNS resolution module. We use the promise-based API for cleaner async/await syntax.

  2. const net = require('net') — Imports Node.js's network utilities, specifically net.isIPv4() for checking if a string is a valid IPv4 address.

  3. isPrivateIP(ip) function — This is the security control. It checks if an IP address falls into reserved/private ranges:
    - 10.0.0.0/8 — Private network range (detected by a === 10)
    - 127.0.0.0/8 — Loopback/localhost (detected by a === 127)
    - 0.0.0.0/8 — "This" network (detected by a === 0)
    - 169.254.0.0/16 — Link-local/metadata endpoints (detected by a === 169 && b === 254)
    - 172.16.0.0/12 — Private range (detected by a === 172 && b >= 16 && b <= 31)
    - 192.168.0.0/16 — Private range (detected by a === 192 && b === 168)
    - IPv6 equivalents — Loopback (::1), link-local (fe80::/10), and unique local (fc00::/7)

  4. const { hostname } = new URL(text) — Safely extracts the hostname from the user-supplied URL using Node.js's built-in URL parser. This is safer than regex parsing.

  5. const { address } = await dns.lookup(hostname) — Performs DNS resolution to convert the hostname to an actual IP address. This is crucial because attackers might use domain names that resolve to private IPs.

  6. if (isPrivateIP(address)) throw 'This URL is not allowed.' — Checks the resolved IP against the private ranges and blocks the request if it matches.

Why DNS Resolution Is Critical

Without DNS resolution, an attacker could bypass IP-based checks using domain names:
- https://internal-db.local/ — Resolves to 10.0.0.5
- https://metadata.local/ — Resolves to 169.254.169.254

The fix handles this by resolving the hostname first, then checking the actual IP.

Prevention & Best Practices

For Your Own Code

  1. Always validate URL destinations, not just formats — Checking URL format is necessary but insufficient. Always resolve hostnames and validate the target IP.

  2. Implement an IP allowlist, not just a blocklist — If your application needs to make external requests, consider whitelisting specific domains or IPs rather than blocking private ranges.

  3. Use established libraries — Consider using libraries like node-fetch-retry or axios with interceptors that can enforce SSRF protections consistently.

  4. Separate concerns — If possible, have a dedicated service for making external HTTP requests with strict controls, rather than allowing arbitrary fetch calls throughout your codebase.

  5. Log and monitor — Log all blocked SSRF attempts. A pattern of SSRF attacks might indicate reconnaissance activity.

Security Standards and References

  • CWE-918: Server-Side Request Forgery (SSRF) — The official vulnerability classification
  • OWASP Top 10 2021 - A06: Vulnerable and Outdated Components — While not directly listed, SSRF is a critical server-side vulnerability
  • OWASP SSRF Prevention Cheat Sheet — Comprehensive guidance on preventing SSRF

Detection Tools

  • Static analysis — Tools like Semgrep can detect patterns where user input flows to fetch/http calls
  • Runtime monitoring — Monitor outbound connections for attempts to reach private IP ranges
  • Network segmentation — Restrict outbound access from application servers to only necessary external services

Key Takeaways

  • Never trust URL format alone — The original code validated that URLs started with http:// or https://, but this provided zero protection against SSRF because attackers could craft valid URLs pointing to internal IPs.

  • Always resolve hostnames before validating — Domain names can resolve to private IPs, so DNS resolution must happen before the IP range check. The fix uses dns.lookup() to convert hostnames to actual IP addresses.

  • Private IP ranges are well-defined — The isPrivateIP() function checks against standardized ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, etc.). These should be hardcoded in any SSRF protection logic.

  • Cloud metadata endpoints are high-value targets — The AWS metadata endpoint at 169.254.169.254 is specifically dangerous because it exposes credentials. The fix explicitly blocks the 169.254.0.0/16 range.

  • Async DNS resolution adds complexity but is necessary — The fix uses await dns.lookup(), making the function async. This is a required change to properly validate hostnames before making requests.

How Orbis AppSec Detected This

Source: The text parameter from the user-supplied WhatsApp command (e.g., .fetch http://169.254.169.254/...)

Sink: The fetch(text) call at line 17 of plugins/tools/fetch.js

Missing control: The code lacked any validation that the URL's resolved IP address was not in a private/reserved range. While the regex check validated URL format, it provided no protection against SSRF.

CWE: CWE-918 — Server-Side Request Forgery (SSRF)

Fix: Added DNS hostname resolution and IP range validation using the isPrivateIP() function to block requests to private, loopback, link-local, and cloud metadata IP ranges before calling fetch().

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

Server-Side Request Forgery vulnerabilities are particularly dangerous in modern cloud environments where metadata endpoints expose sensitive credentials. The original code in fetch.js demonstrated a common mistake: validating URL format while ignoring URL destination. By adding DNS resolution and private IP range validation, the fix ensures that the fetch command can only reach external, public-facing services.

The lesson here extends beyond this specific vulnerability: security controls must validate the actual intent and impact of user input, not just its format. A URL that looks correct might point to a dangerous location. Always resolve, validate, and verify before executing potentially dangerous operations.

For teams maintaining similar code that makes HTTP requests based on user input, implement these controls immediately. SSRF vulnerabilities are trivial to exploit and can lead to complete infrastructure compromise in cloud environments.

References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker tricks a server into making HTTP requests to unintended targets, typically internal resources or cloud metadata services, by controlling the URL parameter.

How do you prevent SSRF in Node.js?

Validate all user-supplied URLs by resolving the hostname to an IP address and checking that it's not in private/reserved IP ranges, not localhost, and not targeting cloud metadata endpoints before making any requests.

What CWE is SSRF?

CWE-918: Server-Side Request Forgery (SSRF), which is also related to CWE-611 (Improper Restriction of XML External Entity Reference) in some contexts.

Is URL format validation enough to prevent SSRF?

No. Checking that a URL starts with `https://` (as the original code did) is insufficient because attackers can craft valid URLs pointing to internal IPs like `https://169.254.169.254/` or `https://192.168.1.1/`.

Can static analysis detect SSRF?

Yes. Static analysis tools can detect when user-controlled input flows directly into fetch() or HTTP request functions without IP range validation, flagging potential SSRF vulnerabilities.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #29

Related Articles

critical

How Server-Side Request Forgery happens in Node.js and how to fix it

The order-flow service in a Node.js e-commerce backend built an outbound fetch() URL by directly concatenating a configurable `sendingOrder.url` value with a query string, with no validation of protocol or destination. This allowed order data—including customer and payment-adjacent information—to be silently redirected to an attacker-controlled endpoint simply by changing a config value or environment variable.

medium

How gitlab.bandit.B501 happens in Python and how to fix it

The `proverbia-scraper.py` script disabled TLS certificate verification on its `requests.get()` call and silenced the resulting security warnings, exposing the scraper to man-in-the-middle attacks. The fix removes the `verify=False` flag and the warning suppression, restoring proper certificate validation while keeping the existing 30-second timeout intact.

high

How Server-Side Request Forgery (SSRF) happens in Node.js and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability in the ldfetch CLI tool allowed attackers to access internal cloud metadata services and local files through unvalidated URL arguments. The fix introduces strict protocol validation with an explicit opt-in flag for local file access, transforming a dangerous default into a secure-by-design implementation.

high

How Server-Side Request Forgery (SSRF) happens in Go HTTP handlers and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict

critical

How SSRF via Vulnerable Dependency Versions Happens in Node.js and How to Fix It

A permissive semver range in `package.json` allowed npm to install axios versions vulnerable to SSRF (CVE-2024-39338). By bumping the minimum version from `^1.6.0` to `^1.7.4`, all downstream consumers of this SDK are now protected from server-side request forgery attacks. This critical fix required changing just one line in the dependency manifest.

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in playground.html where the `__forEachRdfMessageChunkFromUrl` function fetched user-controlled URLs without validating against private IP ranges or internal network addresses. The fix introduces a comprehensive `__isBlockedFetchUrl` validation function that blocks requests to localhost, private IP ranges, and link-local addresses before any fetch occurs.