Back to Blog
critical SEVERITY5 min read

How CSRF vulnerabilities happen in Node.js API clients and how to fix them

A critical CSRF vulnerability in `lib/client.js` allowed attackers to forge authenticated POST requests to `/remote-ssh/api/*` endpoints. The fix adds the `X-Requested-With: XMLHttpRequest` header to enable proper CSRF token validation, blocking malicious cross-site requests with a minimal one-line change.

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

Answer Summary

This is a Cross-Site Request Forgery (CSRF) vulnerability (CWE-352) in a Node.js library's API client (`lib/client.js:294`). The `fetch()` call to `/remote-ssh/api/*` endpoints lacked CSRF protection, relying only on weak `Sec-Fetch-Site` header checks. The fix adds `X-Requested-With: XMLHttpRequest` header to signal legitimate XMLHttpRequest origins, enabling server-side CSRF validation to reject forged cross-origin requests.

Vulnerability at a Glance

cweCWE-352
fixAdded `X-Requested-With: XMLHttpRequest` header to fetch() call at line 294
riskAttackers could forge authenticated API requests to remote SSH endpoints from malicious websites
languageJavaScript/Node.js
root causeMissing `X-Requested-With` header allowed cross-origin POST requests to bypass CSRF checks
vulnerabilityCross-Site Request Forgery (CSRF)

Introduction

The lib/client.js file in this Node.js library handles authenticated communication with remote SSH API endpoints, but a flaw in the fetch() call at line 294 created a critical security risk. The code made POST requests to /remote-ssh/api/* endpoints without including the X-Requested-With header, leaving downstream consumers vulnerable to Cross-Site Request Forgery (CSRF) attacks. While the server-side isTrusted() function attempted validation using Sec-Fetch-Site headers and localhost origin checks, this defense proved inadequate against same-site attacks and legacy browser scenarios.

This vulnerability is particularly dangerous because it affects a library—meaning every application using this package inherited the same exploitable pattern. Developers working on similar API client code should pay close attention to how their requests signal legitimate origin to server-side CSRF protections.

The Vulnerability Explained

The vulnerable code in lib/client.js at line 294 performed a fetch() call without proper CSRF signaling:

// VULNERABLE CODE (before fix)
var r = await fetch("/remote-ssh/api/" + method, {
  method: "POST",
  headers: { "content-type": "application/json" },  // Missing CSRF protection
  body: JSON.stringify(args || {})
});

The critical problem is the missing X-Requested-With header. The server-side isTrusted() function relied on this header (along with Sec-Fetch-Site) to distinguish legitimate XMLHttpRequest calls from forged cross-origin requests. Without it, the server had no reliable way to verify that POST requests to sensitive /remote-ssh/api/* endpoints actually originated from the application's own JavaScript code.

How the attack works: An attacker creates a malicious website that lures authenticated users. When the victim visits the attacker's page, JavaScript triggers a cross-origin POST request to https://victim-app.com/remote-ssh/api/execute-command (or similar endpoints). Because the victim has an active session, their browser automatically includes authentication cookies. The content-type: application/json header bypasses simple preflight requirements, and without X-Requested-With, the server-side isTrusted() check sees only that Sec-Fetch-Site might indicate "cross-site"—but this header is missing in older browsers and can be ambiguous in same-site scenarios. The attacker successfully forges an authenticated API request.

Real-world impact: For applications using this library, attackers could execute arbitrary SSH commands, modify remote configurations, or exfiltrate sensitive data—all without the victim's knowledge or consent.

The Fix

The remediation was elegantly simple: add the X-Requested-With: XMLHttpRequest header to enable proper server-side CSRF validation.

Before:

// lib/client.js:294 (vulnerable)
var r = await fetch("/remote-ssh/api/" + method, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify(args || {})
});

After:

// lib/client.js:294 (fixed)
var r = await fetch("/remote-ssh/api/" + method, {
  method: "POST",
  headers: { "content-type": "application/json", "x-requested-with": "XMLHttpRequest" },
  body: JSON.stringify(args || {})
});

This single-header change transforms the security posture. The X-Requested-With: XMLHttpRequest header is a defense-in-depth mechanism that:

  1. Cannot be set by cross-origin attackers: Browsers restrict JavaScript on attacker-controlled origins from setting this header on cross-origin requests due to CORS preflight requirements
  2. Enables server-side validation: The isTrusted() function can now reliably distinguish legitimate library requests from forged ones
  3. Maintains backward compatibility: The header is standard and safe for all target endpoints

The fix is scoped precisely to the vulnerable path—only the lib/client.js file was modified, ensuring no unintended behavioral changes elsewhere.

Prevention & Best Practices

To prevent CSRF vulnerabilities in API client code:

  • Always include anti-CSRF headers: For XMLHttpRequest/fetch calls, include X-Requested-With: XMLHttpRequest or custom CSRF token headers
  • Implement defense in depth: Don't rely solely on Sec-Fetch-Site; combine multiple validation techniques
  • Use CSRF tokens for state-changing operations: For maximum protection, require cryptographically random tokens in request headers or bodies
  • Validate Origin and Referer headers: Server-side checks should verify these headers against an allowlist
  • Set SameSite cookie attributes: Use SameSite=Strict or SameSite=Lax for session cookies to prevent cross-origin cookie transmission

Detection tools:
- Semgrep rules for CSRF detection: https://semgrep.dev/r?q=csrf
- OWASP CSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- CWE-352 guidance: https://cwe.mitre.org/data/definitions/352.html

Key Takeaways

  • The fetch() call in lib/client.js at line 294 now includes X-Requested-With: XMLHttpRequest to enable CSRF validation
  • Relying on Sec-Fetch-Site alone is insufficient—legacy browsers and same-site scenarios bypass this protection
  • Library code requires extra scrutiny: vulnerabilities propagate to all downstream consumers automatically
  • Single-header security improvements can eliminate entire vulnerability classes without breaking existing functionality
  • Server-side isTrusted() implementations must validate multiple request indicators, not just origin headers

How Orbis AppSec Detected This

Source: HTTP request parameters and body data in lib/client.js fetch calls to /remote-ssh/api/* endpoints

Sink: The fetch() call at lib/client.js:294 performing POST requests without CSRF protection headers

Missing control: No X-Requested-With header or CSRF token validation; server-side isTrusted() relied solely on Sec-Fetch-Site and localhost origin checks

CWE: CWE-352: Cross-Site Request Forgery (CSRF)

Fix: Added x-requested-with: XMLHttpRequest header to the fetch request headers object, enabling server-side CSRF validation to reject cross-origin forged requests.

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

CSRF vulnerabilities in API client libraries pose systemic risks because they affect every application using the package. The fix in lib/client.js demonstrates that effective security improvements need not be complex—a single well-chosen header can close a critical attack vector. When building library code, always consider how your HTTP requests will be validated by server-side protections, and include appropriate security signaling by default. The X-Requested-With: XMLHttpRequest pattern remains a valuable defense-in-depth technique, especially when combined with comprehensive CSRF token validation on the server.

References

  • CWE-352: Cross-Site Request Forgery (CSRF): https://cwe.mitre.org/data/definitions/352.html
  • OWASP CSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
  • MDN: Fetch API - Headers: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#headers
  • Semgrep CSRF rules: https://semgrep.dev/r?q=csrf
  • GitHub PR: fix: add CSRF protection in client.js

Frequently Asked Questions

What is CSRF?

Cross-Site Request Forgery (CSRF) is an attack that tricks authenticated users into submitting unwanted requests to a web application they're logged into, exploiting the browser's automatic cookie/session handling.

How do you prevent CSRF in JavaScript/Node.js?

Include anti-CSRF headers like `X-Requested-With: XMLHttpRequest`, implement CSRF tokens for state-changing operations, validate the `Origin` and `Referer` headers server-side, and use SameSite cookie attributes.

What CWE is CSRF?

CWE-352: Cross-Site Request Forgery (CSRF)

Is checking `Sec-Fetch-Site` enough to prevent CSRF?

No. `Sec-Fetch-Site` is not supported by older browsers and can be bypassed in same-site request scenarios. It should be combined with `X-Requested-With` headers, CSRF tokens, or strict origin validation.

Can static analysis detect CSRF?

Yes. Static analysis can flag missing CSRF protection in API clients, particularly fetch/XHR calls without security headers or CSRF tokens for state-changing operations.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4

Related Articles

critical

How origin validation bypass happens in Express.js and how to fix it

A `POST /changeData` route in `src/main/server/routes/index.js` guarded state-changing writes with an origin allowlist, but the guard was wrapped in an `if (origin && ...)` truthiness check. Any request that simply omitted both `Origin` and `Referer` — a one-line `curl` command, a local script, a background process — skipped validation entirely and modified application data. The fix removes the truthiness short-circuit so a *missing* header is now treated as a rejection, not a pass.

critical

How User Enumeration Happens in Django Forms and How to Fix It

A critical user enumeration vulnerability in the volunteers application allowed attackers to systematically discover registered email addresses through distinct error messages in signup and password reset forms. The fix replaces specific error messages with generic ones, preventing information disclosure while maintaining application functionality.

critical

How Authentication Bypass Happens in Node.js WebSocket Services and How to Fix It

The HousePanel push notification service exposed GET and POST endpoints without any authentication checks, allowing unauthenticated attackers to send arbitrary push notifications to connected smart devices. This critical vulnerability was fixed by implementing mandatory token validation on all protected endpoints, ensuring only authenticated requests can trigger push operations.

critical

How Missing API Authentication Happens in Node.js and How to Fix It

The GitHub API integration in `src/github.mjs` was making unauthenticated requests, subjecting the application to GitHub's strict 60 requests/hour rate limit. This fix adds secure authentication token injection from environment variables using conditional header spreading, enabling authenticated requests with a much higher rate limit (5,000 requests/hour).

high

How OAuth 2.0 Authorization Code Interception happens in PHP and how to fix it

The Weibo OAuth login implementation in `trunk/web/login_weibo.php` was missing PKCE (Proof Key for Code Exchange), allowing attackers with network access to exchange intercepted authorization codes for access tokens. The fix adds cryptographic binding between the authorization request and token exchange using SHA256 code challenges.

critical

How SQL injection happens in Python DuckDB view creation and how to fix it

A critical SQL injection flaw in `python/src/idx/api.py:265` built five DuckDB `CREATE VIEW` statements with Python f-strings, interpolating a filesystem path directly into SQL text. The fix replaces the interpolated path with a bound parameter (`read_parquet(?)`) and moves the view names into a hardcoded, non-interpolated statement map — eliminating any path where filenames or directory values can alter SQL structure.