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 credentialshttp://127.0.0.1:8080/admin- Internal admin panelshttp://10.0.5.20/internal-api- Private network serviceshttp://[::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:
- Loopback blocking: Catches
127.0.0.1,127.1, decimal notation (2130706433), and octal notation (0177.0.0.1) - IPv6 handling: Blocks
::1and IPv4-mapped addresses like::ffff:127.0.0.1 - Hostname resolution: Blocks
localhostregardless of port - Cloud metadata protection: Explicitly blocks
169.254.169.254 - 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
- Allowlist over blocklist: When possible, maintain a list of allowed domains rather than trying to block all dangerous ones
- Disable redirects: Use
{ redirect: 'manual' }and validate redirect targets - Network segmentation: Run services that make outbound requests in isolated network segments
- Egress filtering: Configure firewalls to block outbound requests to internal ranges
- Metadata service protection: Use IMDSv2 on AWS, which requires session tokens
Tools for Detection
- Semgrep: Use rules like
javascript.fetch.security.ssrfto 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.mjsfetch 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.mjsscript from external input (target site configuration or data sources) - Sink:
fetch()call in the fetch wrapper function attemplate/scripts/recon.mjs:60that 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.