Back to Blog
critical SEVERITY6 min read

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

A critical Server-Side Request Forgery (SSRF) vulnerability in the compass-guarded-transfer CLI tool allowed attackers to make HTTP requests to internal services and cloud metadata endpoints. The `normalizeInput` function in `run-transfer.mjs` validated that URLs started with "https://" but failed to prevent requests to private IP ranges like AWS metadata (169.254.169.254) or localhost, enabling potential credential theft and internal network reconnaissance.

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

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in a Node.js CLI tool where the `normalizeInput` function in `run-transfer.mjs` accepted any HTTPS URL without hostname validation, allowing requests to internal services like AWS metadata endpoints (169.254.169.254). The fix adds URL parsing with `new URL()` and validates the hostname against a regex pattern that blocks localhost, private IP ranges (10.x, 192.168.x, 172.16-31.x), link-local addresses (169.254.x), and loopback addresses, preventing SSRF attacks while maintaining legitimate functionality.

Vulnerability at a Glance

cweCWE-918
fixAdded URL parsing and hostname filtering to block private/internal networks
riskAttackers can access internal services, cloud metadata endpoints, and exfiltrate credentials
languageJavaScript (Node.js)
root causeURL validation only checked protocol prefix without hostname restrictions
vulnerabilityServer-Side Request Forgery (SSRF)

Introduction

In the compass-guarded-transfer repository, we discovered a critical Server-Side Request Forgery (SSRF) vulnerability in showcase/compass-guarded-transfer/scripts/run-transfer.mjs. The normalizeInput function at line 35 validated that the compassUrl parameter started with "https://" but failed to prevent requests to internal network addresses. This meant an attacker controlling the command-line arguments could force the application to make HTTP POST requests to AWS metadata endpoints (https://169.254.169.254/latest/meta-data/), internal admin panels (https://localhost:8080/admin), or any other internal service accessible from the server.

The vulnerability existed because the code passed the user-supplied URL directly to the verify() function at line 48, which performs an HTTP POST request without any hostname restrictions. For a CLI tool that processes potentially untrusted input files or environment variables, this created a serious security boundary violation.

The Vulnerability Explained

Let's examine the vulnerable code in run-transfer.mjs:

export function normalizeInput(input) {
  // ... other validation code ...

  if (typeof input.compassUrl !== "string" || !input.compassUrl.startsWith("https://")) 
    stop("Compass HTTPS URL is required");

  return { 
    recipient: input.recipient, 
    amountSol: String(input.amountSol), 
    lamports, 
    amountUsd, 
    cluster: "devnet", 
    compassUrl: input.compassUrl.replace(/\/$/, ""), 
    apiKey: input.apiKey 
  };
}

The problem is on line 45 (in the original code): the validation only checks that compassUrl is a string starting with "https://". This check is insufficient because it accepts any HTTPS URL, including:

  • AWS metadata endpoint: https://169.254.169.254/latest/meta-data/ - could leak IAM credentials
  • Localhost services: https://localhost:8080/admin - could access internal admin interfaces
  • Private network ranges: https://192.168.1.1/config - could scan internal infrastructure
  • Docker metadata: https://172.17.0.1/ - could access container orchestration APIs

Attack Scenario

Here's how an attacker could exploit this vulnerability in the run-transfer.mjs script:

  1. The attacker creates a malicious input file or sets environment variables with:
    json { "compassUrl": "https://169.254.169.254/latest/meta-data/iam/security-credentials/", "recipient": "11111111111111111111111111111111", "amountSol": "0.0001", "amountUsdPolicyInput": "0.01", "confirmed": "yes", "apiKey": "test" }

  2. The normalizeInput function validates the input and returns the malicious URL unchanged

  3. At line 48, the verify() function receives this URL and makes an HTTP POST request to the AWS metadata endpoint

  4. The response contains IAM credentials, which are either logged or returned to the attacker

  5. The attacker now has temporary AWS credentials to access cloud resources

This is particularly dangerous because the compass-guarded-transfer tool handles Solana cryptocurrency transfers. An attacker gaining access to cloud credentials could potentially compromise the entire infrastructure, including wallet keys and transaction signing services.

The Fix

The fix implements comprehensive hostname validation using Node.js's built-in URL constructor and regular expression filtering. Here's the corrected code:

Before:

if (typeof input.compassUrl !== "string" || !input.compassUrl.startsWith("https://")) 
  stop("Compass HTTPS URL is required");

After:

let compassParsed;
try { 
  compassParsed = new URL(input.compassUrl); 
} catch { 
  stop("Compass HTTPS URL is required"); 
}

if (compassParsed.protocol !== "https:") 
  stop("Compass HTTPS URL is required");

const compassHost = compassParsed.hostname;
if (/^(localhost|.*\.local)$/i.test(compassHost) || 
    /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|0\.)/.test(compassHost) || 
    compassHost === "[::1]") 
  stop("Compass URL hostname is not allowed");

How This Fix Works

The fix introduces three layers of defense:

  1. URL Parsing Validation: The new URL(input.compassUrl) constructor throws an exception for malformed URLs. This catches edge cases like https:// (no hostname) or URLs with invalid characters that string prefix checking would miss.

  2. Protocol Verification: Checking compassParsed.protocol !== "https:" ensures the URL uses HTTPS, preventing protocol downgrade attacks.

  3. Hostname Filtering: The regex patterns block:
    - Localhost variants: localhost, *.local domains, and [::1] (IPv6 loopback)
    - Loopback range: 127.x.x.x
    - Private networks: 10.x.x.x, 192.168.x.x
    - Docker default range: 172.16.x.x through 172.31.x.x
    - Link-local addresses: 169.254.x.x (AWS/Azure metadata)
    - Null route: 0.x.x.x

This comprehensive blocklist prevents all common SSRF attack vectors while allowing legitimate external HTTPS URLs to pass through. The fix maintains backward compatibility for valid use cases—any legitimate Compass API endpoint on a public domain will work exactly as before.

Prevention & Best Practices

To prevent SSRF vulnerabilities in Node.js applications:

1. Always Parse and Validate URLs

Never rely on string operations like startsWith() for URL validation. Use the built-in URL constructor:

// ❌ Bad: String checking
if (url.startsWith("https://")) { /* ... */ }

// ✅ Good: Proper parsing
try {
  const parsed = new URL(url);
  if (parsed.protocol !== "https:") throw new Error("HTTPS required");
} catch (e) {
  throw new Error("Invalid URL");
}

2. Implement Hostname Allowlists

For maximum security, use an allowlist of permitted domains rather than a blocklist:

const ALLOWED_DOMAINS = ['api.compass.example.com', 'compass-prod.example.com'];
const hostname = new URL(url).hostname;
if (!ALLOWED_DOMAINS.includes(hostname)) {
  throw new Error("Domain not allowed");
}

3. Block Private IP Ranges

If allowlisting isn't feasible, always block private networks:

function isPrivateIP(hostname) {
  return /^(localhost|.*\.local)$/i.test(hostname) ||
         /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|0\.)/.test(hostname) ||
         hostname === "[::1]";
}

4. Use Network-Level Controls

Deploy defense-in-depth by restricting outbound network access:
- Configure firewall rules to block requests to private IP ranges
- Use VPC security groups to limit egress traffic
- Implement network segmentation to isolate sensitive services

5. Disable URL Redirects

HTTP clients should not follow redirects when making requests to user-supplied URLs, as attackers can use redirects to bypass hostname validation:

fetch(url, { redirect: 'manual' })

6. Log and Monitor Outbound Requests

Implement logging for all HTTP requests made to external URLs:
- Log the destination hostname and IP
- Alert on requests to private IP ranges
- Monitor for unusual patterns (e.g., requests to metadata endpoints)

Security Standards

This vulnerability maps to:
- CWE-918: Server-Side Request Forgery (SSRF)
- OWASP Top 10 2021: A10:2021 – Server-Side Request Forgery (SSRF)
- OWASP ASVS 4.0: V5.2.6 - URL validation requirements

Key Takeaways

  • The normalizeInput function's string prefix check (startsWith("https://")) was insufficient to prevent SSRF attacks because it didn't validate the hostname component of the URL
  • AWS metadata endpoint (169.254.169.254) and other link-local addresses must be explicitly blocked in any application that makes HTTP requests to user-controlled URLs
  • The new URL() constructor in Node.js provides robust parsing and should always be used instead of string operations for URL validation
  • The regex pattern blocking private IP ranges (/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|0\.)/.test(compassHost)) is essential for preventing internal network reconnaissance
  • CLI tools that process input files or environment variables must treat all external input as untrusted, even if the tool is intended for "local use only"

How Orbis AppSec Detected This

  • Source: The compassUrl parameter from user input (command-line arguments or configuration files)
  • Sink: The verify() function at line 48 in run-transfer.mjs, which makes an HTTP POST request to the user-supplied URL
  • Missing control: No hostname validation to prevent requests to private IP ranges, localhost, or cloud metadata endpoints
  • CWE: CWE-918 (Server-Side Request Forgery)
  • Fix: Added URL parsing with the URL constructor and hostname filtering using regex patterns to block private networks, localhost variants, and link-local addresses

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 run-transfer.mjs demonstrates why comprehensive URL validation is critical, even in CLI tools. The fix transforms a dangerous pattern—accepting any HTTPS URL—into a secure implementation that blocks private networks while maintaining legitimate functionality. By using proper URL parsing with new URL() and implementing hostname filtering against private IP ranges, the code now prevents attackers from accessing internal services, cloud metadata endpoints, and other sensitive resources.

The key lesson: protocol validation alone is never sufficient. Always validate the hostname component of URLs, especially when making HTTP requests based on user input. Implement defense-in-depth with allowlists, blocklists, network controls, and monitoring to protect against SSRF attacks.

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 destinations, typically internal services or cloud metadata endpoints that should not be accessible externally.

How do you prevent SSRF in Node.js?

Validate URLs using the URL constructor, implement hostname allowlists or blocklists for private IP ranges (10.x, 192.168.x, 172.16-31.x, 169.254.x), localhost, and loopback addresses, and avoid making requests to user-controlled URLs without validation.

What CWE is Server-Side Request Forgery?

SSRF is classified as CWE-918: Server-Side Request Forgery. It's part of the broader category of injection vulnerabilities where untrusted input influences server-side operations.

Is checking for HTTPS protocol enough to prevent SSRF?

No. Checking only the protocol (https://) prevents some attacks but doesn't stop requests to internal HTTPS services, cloud metadata endpoints, or localhost. You must also validate the hostname against a blocklist of private networks.

Can static analysis detect SSRF vulnerabilities?

Yes. Modern static analysis tools and AI-powered scanners can detect SSRF by tracking data flow from user input to HTTP request functions and identifying missing hostname validation, as demonstrated by the multi_agent_ai scanner that flagged this vulnerability.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #106

Related Articles

high

How Message-Level Raw Option Bypass happens in Node.js Nodemailer and how to fix it

A high-severity vulnerability in Nodemailer (versions before 9.0.0) allowed the `raw` message option to completely bypass `disableFileAccess` and `disableUrlAccess` security controls, enabling attackers to read arbitrary files from the server filesystem and perform full-response Server-Side Request Forgery (SSRF) in delivered email messages. Upgrading from `^8.0.10` to `^9.0.4` in `backend/package-lock.json` closes this exploit primitive by enforcing access restrictions consistently across all m

high

How Octal/Decimal IP Parsing Ambiguity happens in JavaScript and how to fix it

CVE-2026-69192 is a high-severity vulnerability in the `ip-address` npm package (versions before 10.3.1) where IPv4 addresses with leading-zero octets — like `010.0.0.1` — are parsed as decimal by the library but interpreted as octal by OS-level resolvers, creating a dangerous mismatch. This discrepancy can allow attackers to bypass IP-based access controls and trust boundaries, potentially enabling Server-Side Request Forgery (SSRF) attacks. Upgrading to `ip-address@10.3.1` in the SAP BW Query

critical

How unvalidated URL input handling happens in SvelteKit with Tauri and how to fix it

A critical vulnerability in `src/routes/+page.svelte` allowed attackers to supply arbitrary URLs—including `http://` and local file paths—through query parameters and drag-drop events, which were then fetched without validation. The fix restricts input to HTTPS-only URLs and removes the dangerous local file fetch path entirely, eliminating both SSRF and local file disclosure attack vectors.

critical

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

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `server.js` and `worker.js`, where user-supplied `config` URL parameters were passed directly to `fetchWithAuth()` without any validation. This allowed attackers to force the application to make requests to internal network addresses, cloud metadata endpoints like `169.254.169.254`, or `file://` URIs. The fix introduces an `isAllowedUrl()` allowlist function that rejects private IP ranges, loopback addresses, and non-HTTP(S) pr

high

How Nodemailer raw option bypass happens in Node.js and how to fix it

A high-severity vulnerability in Nodemailer versions prior to 9.0.1 allowed attackers to bypass the `disableFileAccess` and `disableUrlAccess` security controls using the message-level `raw` option. This bypass enabled arbitrary file reads from the server and full-response Server-Side Request Forgery (SSRF) attacks, potentially exposing sensitive configuration files and internal network resources. The fix involves upgrading Nodemailer from version 8.0.7 to 9.0.1.

critical

How Credential Leakage in GitHub Actions Happens in Node.js and How to Fix It

A GitHub Actions workflow in Node.js was storing authentication tokens in plain variables without masking them in logs, creating a critical security risk. When debug mode was enabled or errors occurred, tokens could be exposed in console output and GitHub Actions logs. The fix uses the `setSecret()` API to automatically mask sensitive credentials throughout the execution.