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

high

How IP Address Parsing Inconsistencies Cause SSRF and Trust-Boundary Bypass in Node.js Applications

The `ip-address` library version 10.2.0 contained a critical parsing inconsistency where the `Address4` decoder interpreted leading-zero octets as decimal numbers, while most DNS resolvers and network systems interpreted them as octal. This mismatch allowed attackers to bypass IP-based access controls and SSRF filters. Upgrading to version 10.3.1 fixes this vulnerability by aligning the library's parsing behavior with standard resolver behavior.

critical

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

A critical SSRF vulnerability was discovered in server.js where the API proxy endpoint constructed target URLs from user-controlled path parameters without validating the final origin. Attackers could use URL encoding tricks like `/api/%2F%2Fevil.com` to redirect proxy requests to arbitrary hosts, potentially accessing cloud metadata services or internal resources. The fix adds origin validation to ensure all proxied requests only reach the intended openrouter.ai upstream.

high

How Octal vs. Decimal IP Address Parsing Inconsistency Enables SSRF in Node.js and How to Fix It

The `ip-address` npm package (version 10.2.0) parsed IPv4 addresses with leading-zero octets as decimal numbers, while operating system resolvers interpret them as octal. This inconsistency (CVE-2026-69192) allows attackers to bypass SSRF protections and trust-boundary checks by crafting IP addresses that appear safe to the library but resolve to internal network addresses. The fix upgrades `ip-address` to version 10.3.1, which correctly rejects or normalizes ambiguous octal notation.

high

How SSRF and Credential Leakage happens in Node.js axios and how to fix it

CVE-2025-27152 is a high-severity vulnerability in axios versions prior to 1.8.2 that allows Server-Side Request Forgery (SSRF) and credential leakage when absolute URLs are passed in requests. By upgrading from the vulnerable `^1.7.4` range (which resolved to `1.7.9`) to the pinned `1.8.2`, the attack surface for intercepting or redirecting authenticated HTTP requests is eliminated. Any Node.js application that passes user-influenced URLs to axios is potentially affected.

critical

How Server-Side Request Forgery happens in Browser Extensions and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability in `offscreen.js` allowed attackers to supply malicious feed URLs that the browser extension would fetch without validation, potentially exposing internal network services including cloud metadata endpoints. The fix introduces a dedicated `validateFeedUrl` utility and disables automatic redirect following, closing the attack vector before requests leave the extension. This kind of vulnerability is especially dangerous in browser extensions becau

critical

How CORS Misconfiguration happens in Node.js with Hono and how to fix it

CVE-2026-54290 is a HIGH severity CORS misconfiguration in the Hono web framework where the CORS middleware incorrectly reflects any `Origin` header back to the client — including credentials — when the `origin` option defaults to a wildcard. Upgrading `hono` from `4.12.16` to `4.12.34` in `package-lock.json` and pinning the version via `overrides` in `package.json` closes the vulnerability. Left unpatched, this flaw could allow malicious cross-origin sites to make credentialed requests and read