Back to Blog
critical SEVERITY6 min read

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

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in the `recon.mjs` script, where a fetch wrapper accepted arbitrary URLs without validation. This allowed attackers to access internal infrastructure and cloud metadata services. The fix implements comprehensive URL validation that blocks internal IP ranges, loopback addresses, and dangerous protocols before any network request is made.

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

Answer Summary

Server-Side Request Forgery (SSRF) in Node.js occurs when a fetch wrapper accepts arbitrary URLs without validation, allowing attackers to access internal services like cloud metadata endpoints. This vulnerability (CWE-918) was found in `recon.mjs` where URLs passed to fetch were not validated against internal IP ranges. The fix implements URL validation that blocks loopback addresses (127.0.0.1, localhost, ::1), RFC1918 private ranges (10.x.x.x, 192.168.x.x), and cloud metadata IPs (169.254.169.254) before any request is made.

Vulnerability at a Glance

cweCWE-918
fixImplement URL validation that blocks internal IPs, loopback addresses, and dangerous protocols before fetch execution
riskAttackers can access internal services, cloud metadata, and bypass network security controls
languageJavaScript (Node.js)
root causeFetch wrapper accepts arbitrary URLs without validating against internal IP ranges or blocked protocols
vulnerabilityServer-Side Request Forgery (SSRF)

Introduction

The recon.mjs script in this Node.js library serves as a site crawler for migration workflows, fetching and analyzing content from target URLs. However, a critical flaw in its fetch wrapper function created a severe security risk: the script accepted arbitrary URLs without any validation, enabling Server-Side Request Forgery (SSRF) attacks that could compromise internal infrastructure.

At line 60 of template/scripts/recon.mjs, the fetch wrapper blindly followed any URL provided to it—including internal IP addresses, cloud metadata endpoints, and localhost services. For a library consumed by downstream applications, this vulnerability meant that any attacker who could influence the target URL parameter could pivot through the server to access protected internal resources.

The Vulnerability Explained

Server-Side Request Forgery occurs when an application makes HTTP requests to URLs controlled by an attacker. In recon.mjs, the fetch wrapper was designed to crawl websites during migration, but it lacked any safeguards against malicious destinations.

The vulnerable pattern looked like this:

// Vulnerable: fetch wrapper accepts any URL without validation
async function fetchPage(url) {
  const response = await fetch(url, { redirect: 'follow' });
  return response;
}

This code has three critical problems:
1. No URL validation - Any URL is accepted, including internal addresses
2. Automatic redirect following - Attackers can use open redirects to bypass initial checks
3. No protocol restrictions - File and other dangerous protocols might be accessible

Attack Scenario

Consider an attacker who can control the target site being migrated or compromise a data source that feeds URLs to the recon script. They could provide URLs like:

  • http://169.254.169.254/latest/meta-data/ - AWS instance metadata containing IAM credentials
  • http://127.0.0.1:8080/admin - Internal admin panels
  • http://10.0.5.20/internal-api - Private network services
  • http://[::ffff:127.0.0.1]/ - IPv4-mapped IPv6 loopback bypass

The attacker doesn't need direct access to internal services—they simply trick the server into making requests on their behalf, effectively using the application as a proxy into the internal network.

Real-World Impact

For this Node.js library, the impact extends to every downstream consumer:
- Cloud credential theft: AWS, GCP, and Azure all expose instance metadata at 169.254.169.254
- Internal service enumeration: Attackers can probe internal networks for vulnerable services
- Data exfiltration: Internal APIs may return sensitive data without authentication when accessed from "trusted" internal IPs
- Lateral movement: Compromised credentials or tokens enable further attacks

The Fix

The fix implements a comprehensive SSRF guard that validates URLs before any fetch request is made. The key insight is that all dangerous URLs can be blocked at the URL parsing stage, without requiring network access.

New Test Coverage

The PR adds extensive test cases that verify the SSRF guard blocks all known bypass techniques:

describe('recon — SSRF guard');

gate('no target at all is a usage error', {
  script: 'recon.mjs',
  files: {},
  args: [],
  expect: 1,
  contains: 'usage:',
});

for (const [label, target] of [
  ['loopback', 'http://127.0.0.1/'],
  ['loopback, written short', 'http://127.1/'],
  ['loopback, written as an integer', 'http://2130706433/'],
  ['loopback, written in octal', 'http://0177.0.0.1/'],
  ['loopback over IPv6', 'http://[::1]/'],
  ['loopback as IPv4-mapped IPv6', 'http://[::ffff:127.0.0.1]/'],
  ['localhost by name', 'http://localhost:8080/'],
  ['the cloud metadata address', 'http://169.254.169.254/latest/meta-data/'],
  ['an RFC1918 address', 'http://10.0.5.20/'],
  ['a private 192.168 address', ...],
]) {
  // Each blocked URL causes exit code 1
}

What Changed

The fix removes recon.mjs from the UNCOVERED list (line 952 in the diff), indicating it now has proper test coverage:

 const UNCOVERED = {
   'verify.mjs': NETWORK,
-  'recon.mjs': NETWORK,
   'shots.mjs': NETWORK,

The SSRF guard implements these protections:

  1. Loopback blocking: Catches 127.0.0.1, 127.1, decimal notation (2130706433), and octal notation (0177.0.0.1)
  2. IPv6 handling: Blocks ::1 and IPv4-mapped addresses like ::ffff:127.0.0.1
  3. Hostname resolution: Blocks localhost regardless of port
  4. Cloud metadata protection: Explicitly blocks 169.254.169.254
  5. Private range blocking: Blocks RFC1918 addresses (10.x.x.x, 192.168.x.x, 172.16-31.x.x)

Important Limitation

As noted in the code comments, redirect-based SSRF bypasses cannot be tested offline:

/* ⚠ THE REDIRECT REFUSAL IS NOT COVERED HERE and cannot be: it needs a real
 *   server issuing a 302 to an internal address. It does not exit 1 — it
 *   returns null and adds a note — so the ledger does not demand it, but do
 *   not read this block as proof that the redirect path works.
 */

This is an honest acknowledgment that while the initial URL validation is thoroughly tested, redirect-following behavior requires integration testing with live servers.

Prevention & Best Practices

URL Validation Pattern

Always validate URLs before making server-side requests:

function isUrlSafe(urlString) {
  const url = new URL(urlString);

  // Block dangerous protocols
  if (!['http:', 'https:'].includes(url.protocol)) {
    return false;
  }

  // Resolve hostname to IP and check ranges
  const ip = resolveHostname(url.hostname);
  if (isInternalIP(ip)) {
    return false;
  }

  return true;
}

Defense in Depth

  1. Allowlist over blocklist: When possible, maintain a list of allowed domains rather than trying to block all dangerous ones
  2. Disable redirects: Use { redirect: 'manual' } and validate redirect targets
  3. Network segmentation: Run services that make outbound requests in isolated network segments
  4. Egress filtering: Configure firewalls to block outbound requests to internal ranges
  5. Metadata service protection: Use IMDSv2 on AWS, which requires session tokens

Tools for Detection

  • Semgrep: Use rules like javascript.fetch.security.ssrf to detect unvalidated fetch calls
  • ESLint plugins: Security-focused plugins can flag dangerous patterns
  • Dynamic testing: Tools like Burp Suite can test for SSRF during penetration testing

Key Takeaways

  • The recon.mjs fetch wrapper required validation before any URL was passed to fetch() - this is the fundamental fix
  • IP address bypass techniques are numerous - blocking "127.0.0.1" alone is insufficient; you must handle decimal, octal, IPv6, and mapped address formats
  • Cloud metadata endpoints (169.254.169.254) are prime SSRF targets - always explicitly block this address range
  • Library vulnerabilities affect all downstream consumers - this Node.js library's SSRF exposed every application that used it
  • Test coverage must include bypass attempts - the fix includes tests for 10+ different loopback representations

How Orbis AppSec Detected This

  • Source: URL parameter passed to the recon.mjs script from external input (target site configuration or data sources)
  • Sink: fetch() call in the fetch wrapper function at template/scripts/recon.mjs:60 that follows redirects automatically
  • Missing control: No URL validation, IP range blocking, or protocol allowlisting before the fetch request
  • CWE: CWE-918 (Server-Side Request Forgery)
  • Fix: Implemented comprehensive URL validation that blocks internal IP ranges, loopback addresses (including bypass variants), cloud metadata endpoints, and validates protocols before any fetch is executed

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

This SSRF vulnerability in recon.mjs demonstrates why URL validation is critical for any server-side code that makes HTTP requests based on external input. The fix shows that comprehensive protection requires handling numerous IP address representations and bypass techniques—simple string matching against "localhost" or "127.0.0.1" is never sufficient.

For developers building similar functionality, the key lesson is to validate URLs at the parsing stage, before any network request is made. Use established libraries for IP range checking, explicitly block cloud metadata addresses, and consider using allowlists when the set of valid destinations is known.

References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker can make a server-side application send HTTP requests to arbitrary destinations, potentially accessing internal services, cloud metadata endpoints, or other protected resources that should not be externally accessible.

How do you prevent SSRF in Node.js?

Prevent SSRF by validating URLs before making requests: block internal IP ranges (127.0.0.1, 10.x.x.x, 192.168.x.x, 169.254.x.x), use URL allowlists, disable automatic redirect following, and validate protocols to only allow http/https.

What CWE is SSRF?

SSRF is classified as CWE-918: Server-Side Request Forgery (SSRF), which describes vulnerabilities where an attacker can abuse server functionality to make requests to unintended locations.

Is blocking localhost enough to prevent SSRF?

No, blocking localhost alone is insufficient. Attackers can bypass this using alternative representations like 127.1, 0177.0.0.1 (octal), 2130706433 (decimal), IPv6 addresses (::1, ::ffff:127.0.0.1), or DNS rebinding attacks. Comprehensive IP range validation is required.

Can static analysis detect SSRF?

Yes, static analysis tools can detect SSRF by tracing data flow from user-controlled inputs to HTTP request functions. Tools like Semgrep, CodeQL, and specialized security scanners can identify fetch/request calls that accept unvalidated URLs.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

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.