Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | commit adding isAllowedBoard() validation |
| Ecosystem | Node.js |
| CVE / GHSA | not assigned |
| CWE | CWE-918 (Server-Side Request Forgery) |
The Vulnerability Explained
The boardcards.js utility accepts a --board command-line parameter to specify which board service endpoint to query. The parseArgs() function processed this input with a critical gap: it accepted any string value and passed it through to subsequent fetch() operations without validating whether the URL was safe to request.
Here's the vulnerable pattern:
function parseArgs(argv) {
const opts = {
board: process.env.BOARD_ORIGIN || 'http://127.0.0.1:3100',
// ...
};
// ...
opts.board = String(opts.board || '').replace(/\/+$/, '');
if (!opts.board) {
throw new Error('--board wants an origin');
}
return opts;
}
The problem sits at the validation boundary. The code only checked for presence (!opts.board), not for safety. An attacker controlling the --board argument could supply:
http://169.254.169.254/latest/meta-data/iam/security-credentials/— AWS EC2 instance metadatahttp://metadata.google.internal/computeMetadata/v1/— Google Cloud metadatahttp://10.0.0.1:22/— Internal SSH services for port scanningfile:///etc/passwd— Local file access (depending on fetch implementation)
Since the board URL is used in downstream fetch() calls, this becomes a textbook Server-Side Request Forgery vulnerability. In cloud deployments, this routinely leads to IAM credential exposure and lateral movement.
The Fix
The patch introduces isAllowedBoard(), a purpose-built validation function that enforces two security boundaries:
function isAllowedBoard(urlStr) {
let u;
try { u = new URL(urlStr); } catch { return false; }
if (u.protocol !== 'http:' && u.protocol !== 'https:') { return false; }
if (u.hostname === '169.254.169.254' || u.hostname === 'metadata.google.internal') { return false; }
return true;
}
The validation now rejects:
- Non-HTTP(S) protocols (blocking file://, ftp://, data://, etc.)
- Known cloud metadata endpoints by hostname
The call site was updated to use this validator:
if (!opts.board || !isAllowedBoard(opts.board)) {
throw new Error('--board wants a plain http(s) origin, not a link-local/metadata address');
}
This change transforms an open SSRF into a restricted, allowlist-based validation. The error message improvement also aids debugging—users now understand why their URL was rejected.
Key Takeaways
-
Command-line arguments are untrusted input: Even CLI tools face SSRF risks when parameters become URLs. Treat
process.argvwith the same skepticism as HTTP query parameters. -
Environment variables need validation too: The
BOARD_ORIGINfallback means environment-sourced values must pass the sameisAllowedBoard()check as explicit--boardarguments. -
Protocol validation prevents protocol smuggling: Rejecting non-HTTP(S) schemes closes avenues for
file://reads,data://injection, and other fetch-based attacks. -
Metadata endpoints are high-value targets: Explicitly blocking
169.254.169.254andmetadata.google.internaladdresses the most common cloud SSRF exploitation pattern. -
Error messages are security UX: The updated error message educates users about valid input patterns while revealing nothing exploitable to attackers.
How Orbis AppSec Detected This
Source: The --board command-line argument accepted by parseArgs()
Sink: fetch() calls using the unsanitized board URL
Missing control: No validation of URL scheme, hostname, or IP address before network request initiation
CWE: CWE-918 — Server-Side Request Forgery (SSRF)
Fix: Added isAllowedBoard() function enforcing HTTP(S) protocol requirement and explicit denylist for cloud metadata hostnames, with updated error messaging in parseArgs().
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 vulnerability demonstrates that SSRF isn't limited to web applications with HTTP frontends. Any code that accepts URLs from user input—whether via CLI flags, environment variables, or configuration files—must validate those URLs before making network requests. The isAllowedBoard() pattern provides a template: parse strictly, whitelist protocols, and explicitly deny known-sensitive hostnames.