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.

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #106

Related Articles

high

ip-address 10.2.0 SSRF: Inconsistent Parsing Bypasses IP Checks

The `ip-address` npm package version 10.2.0 contains an inconsistent parsing vulnerability that allows attackers to bypass IP-based access controls. By representing IPv4 addresses in IPv4-mapped IPv6 notation, attackers can trick applications into allowing requests to blocked internal addresses. Upgrading to 10.3.1 resolves this through stricter address normalization.

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 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 happens in Python FastAPI and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.

critical

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

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `maintenance/getImages.js`, where the `getImage()` function passed database-sourced URLs directly to `axios.get()` without any validation. An attacker who could modify the elements database could redirect these requests to internal network resources — including AWS cloud metadata endpoints — potentially exposing IAM credentials and other sensitive infrastructure data. The fix introduces a strict URL allowlist that limi