How Server-Side Request Forgery (SSRF) happens in Node.js and how to fix it
In the ldfetch repository, we discovered a high severity Server-Side Request Forgery (SSRF) vulnerability in bin/ldfetch.js that allowed attackers to pivot through the CLI tool and access internal cloud infrastructure. The vulnerability resided in how the tool handled its primary command-line argument—a URL that was passed directly to the fetcher without any validation of protocol, destination, or intent.
What makes this case particularly instructive is how a seemingly simple command-line utility, designed to fetch Linked Data resources from the web, became a potential gateway for cloud credential theft and internal network reconnaissance.
The Vulnerability Explained
The ldfetch CLI tool is designed to retrieve RDF data from URLs. Users invoke it with:
node bin/ldfetch.js https://example.org/data.ttl
The vulnerability existed in the argument handling code at lines 16-21 of bin/ldfetch.js:
program
.option('-p, --predicates <predicates ...>', 'Some predicates can be followed [predicates]', list)
.option('--frame <jsonldframe|file>', 'Add a JSON-LD frame')
.arguments('<url>')
.action(function (argUrl) {
//TODO: check whether starts with http(s)? <-- The TODO that never happened
url = argUrl;
})
.parse(process.argv);
The commented TODO—//TODO: check whether starts with http(s)?—is a stark reminder of how postponed security checks become exploitable vulnerabilities. The url variable was captured from user input and later used to initialize fetch operations without any intermediate validation.
The fetcher initialization at line 40 compounded the problem:
var fetch = new ldfetch(); // No restrictions on what URLs could be fetched
This meant an attacker could execute:
# Steal AWS IAM credentials from EC2 metadata service
node bin/ldfetch.js http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Access internal admin panels
node bin/ldfetch.js http://localhost:8080/admin
# Read arbitrary local files
node bin/ldfetch.js file:///etc/passwd
The 169.254.169.254 IP address is particularly dangerous—it's the link-local address used by AWS, Azure, and GCP for instance metadata services. Successful exploitation would yield temporary IAM credentials, potentially granting attackers persistent access to cloud infrastructure.
The Fix
The remediation transformed the security model from permissive-by-default to secure-by-design with explicit opt-ins for dangerous functionality.
Protocol Validation Module
The fix introduces a dedicated validation module (lib/allowedProtocol.js) imported at line 4:
var isAllowedProtocol = require('../lib/allowedProtocol.js');
Restricted Fetcher Initialization
The fetcher is now initialized with explicit security controls at line 30:
var fetch = new ldfetch({ localFiles: !!options.localFiles });
The localFiles option is only enabled when users explicitly pass the --local-files flag, making dangerous functionality a conscious security decision.
Explicit Local File Opt-In
A new command-line option was added at line 19:
.option('-l, --local-files', 'Allow fetching file:// URLs (disabled by default; only use with trusted input)')
This follows the principle of secure defaults: file:// URLs are blocked unless explicitly requested, and the help text warns users to "only use with trusted input."
Before vs. After
| Aspect | Before (Vulnerable) | After (Fixed) |
|---|---|---|
| URL validation | None—any URL accepted | Protocol whitelist enforced |
file:// access |
Always allowed | Requires --local-files flag |
| Fetcher config | new ldfetch() |
new ldfetch({ localFiles: !!options.localFiles }) |
| User intent | Implicit | Explicit for dangerous operations |
The documentation was also updated to clarify the new security behavior:
By default, only `http://` and `https://` URLs are fetched. Pass `--local-files`
to also allow `file://` URLs... (disabled by default for security; only use with
trusted input).
Prevention & Best Practices
SSRF Defense in Depth
-
Protocol Whitelisting: Restrict allowed URL schemes to only those necessary (
http://,https://). Never allowfile://,ftp://,gopher://, or other schemes unless absolutely required. -
IP Range Denylists: Block requests to private IP ranges (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8) and cloud metadata addresses (169.254.169.254). -
DNS Rebinding Protection: Resolve hostnames to IPs before validation and re-verify after DNS resolution to prevent rebinding attacks.
-
Principle of Least Privilege: Run services with minimal network access. The ldfetch fix embodies this by requiring explicit flags for elevated privileges.
Detection Tools
- Static Analysis: Tools like Semgrep, CodeQL, and the
multi_agent_aiscanner used here can detect tainted input reaching URL fetch operations - OWASP ZAP: Dynamic testing for SSRF vulnerabilities
- Cloud Security Posture Management (CSPM): Detect overprivileged instance metadata access
Relevant Standards
- CWE-918: Server-Side Request Forgery (SSRF)
- OWASP Top 10 2021: A10:2021 – Server-Side Request Forgery (SSRF)
- OWASP SSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
Key Takeaways
- TODO comments are security debt: The
//TODO: check whether starts with http(s)?comment identified the exact vulnerability that was later exploited—security checks should never be postponed - The
ldfetchconstructor now requires explicitlocalFilesconfiguration: Default-deny is enforced through!!options.localFiles, ensuringfile://access is opt-in only - Command-line tools need the same validation as web applications: CLI arguments are untrusted input and require identical sanitization to HTTP parameters
- Cloud metadata services are high-value SSRF targets: The
169.254.169.254address should be explicitly blocked in all URL-handling code - Documentation drives secure behavior: The README update explaining the
--local-filesflag helps users understand security implications
How Orbis AppSec Detected This
| Field | Details |
|---|---|
| Source | Command-line argument argUrl captured in program.action() at bin/ldfetch.js:20 |
| Sink | new ldfetch() instantiation and subsequent fetch operations on unvalidated URLs |
| Missing control | No protocol validation, no IP range restrictions, no destination allowlisting |
| CWE | CWE-918: Server-Side Request Forgery (SSRF) |
| Fix | Implemented protocol whitelist via isAllowedProtocol module and added explicit --local-files opt-in flag with secure default-deny behavior |
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
The ldfetch SSRF vulnerability demonstrates how even simple CLI tools can become attack vectors when they handle URLs without validation. The fix's elegance lies in its minimal footprint—adding a protocol check and an explicit flag—while fundamentally changing the security posture from permissive to restrictive.
For developers building similar tools, the lesson is clear: treat all URL inputs as potentially malicious. Whether from HTTP headers, form fields, or command-line arguments, URLs can target internal infrastructure, cloud metadata services, and local file systems. The secure-by-default pattern used here—requiring explicit flags for dangerous functionality—should be your standard approach.
References
- CWE-918: Server-Side Request Forgery (SSRF): https://cwe.mitre.org/data/definitions/918.html
- OWASP SSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- AWS Metadata Service documentation and security best practices: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
- Semgrep rule for SSRF detection: https://semgrep.dev/r?q=ssrf
- fix: fix security issue in ldfetch.js