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:
-
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" } -
The
normalizeInputfunction validates the input and returns the malicious URL unchanged -
At line 48, the
verify()function receives this URL and makes an HTTP POST request to the AWS metadata endpoint -
The response contains IAM credentials, which are either logged or returned to the attacker
-
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:
-
URL Parsing Validation: The
new URL(input.compassUrl)constructor throws an exception for malformed URLs. This catches edge cases likehttps://(no hostname) or URLs with invalid characters that string prefix checking would miss. -
Protocol Verification: Checking
compassParsed.protocol !== "https:"ensures the URL uses HTTPS, preventing protocol downgrade attacks. -
Hostname Filtering: The regex patterns block:
- Localhost variants:localhost,*.localdomains, 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.xthrough172.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
normalizeInputfunction'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
compassUrlparameter from user input (command-line arguments or configuration files) - Sink: The
verify()function at line 48 inrun-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
URLconstructor 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.