Back to Blog
high SEVERITY8 min read

How CSRF protection gaps happen in Express.js applications and how to fix it

A high-severity security vulnerability was discovered in a React-Express booking application where the Express backend lacked CSRF middleware protection, while the frontend's coupon code input field in `Listing.jsx` allowed unrestricted user input. The fix implemented strict input validation using a regex pattern that whitelists only alphanumeric characters, hyphens, and underscores, preventing malicious payloads from reaching backend database operations.

O
By Orbis AppSec
Published August 16, 2026Reviewed August 16, 2026

Answer Summary

This vulnerability involves missing CSRF (Cross-Site Request Forgery) protection in an Express.js backend combined with insufficient input validation on user-controlled fields (CWE-352 and CWE-20). In the booking application's `Listing.jsx` component at line 133, the coupon code input field accepted any characters, allowing attackers to inject SQL, HTML, or script tags that could flow to database operations. The fix adds a regex filter `.replace(/[^A-Z0-9\-_]/g, "")` to the `onChange` handler, restricting input to uppercase alphanumeric characters, hyphens, and underscores only.

Vulnerability at a Glance

cweCWE-352 (CSRF), CWE-20 (Improper Input Validation)
fixAdded regex-based input filtering to whitelist only safe characters (A-Z, 0-9, -, _)
riskAttackers can inject malicious payloads through coupon codes reaching database operations
languageJavaScript (Express.js backend, React frontend)
root causeNo CSRF token validation in Express backend; frontend coupon input lacks sanitization
vulnerabilityMissing CSRF middleware + Insufficient input validation

The Discovery: A Dual-Layer Security Gap

In a production booking application's codebase, security analysis uncovered a critical compound vulnerability affecting both the Express.js backend (backend/api.js) and the React frontend (frontend/src/components/Booking/Listing.jsx). The Express application was running without any CSRF middleware protection, while simultaneously, the frontend's coupon code input field at line 133 was accepting unrestricted user input. This combination created a dangerous attack surface where malicious payloads could flow directly from user input to backend database operations.

The vulnerability was flagged as high severity because it represented two missing security controls: no CSRF token validation at the application level, and no input sanitization at the point of data entry. For developers building Express-React applications with user input fields—especially those handling promotional codes, user profiles, or any form data—this case demonstrates how frontend validation gaps can compound backend security weaknesses.

The Vulnerable Code Pattern

Let's examine the specific vulnerable code in frontend/src/components/Booking/Listing.jsx at line 133:

<input
  type="text"
  placeholder="Enter code (e.g. SUMMER20)"
  value={couponInput}
  onChange={(e) => setCouponInput(e.target.value.toUpperCase())}
  className="flex-1 px-3 py-2 rounded-xl border border-gray-300..."
/>

The problem here is subtle but critical. The onChange handler calls e.target.value.toUpperCase(), which converts input to uppercase but does nothing to filter out dangerous characters. This means a user could type:

  • SUMMER20'; DROP TABLE bookings;-- (SQL injection attempt)
  • <script>alert('XSS')</script> (Cross-site scripting payload)
  • SUMMER20<img src=x onerror=alert(1)> (HTML injection)
  • SUMMER20${process.env.SECRET} (Template injection attempt)

All of these payloads would be accepted, uppercased, and sent to the backend. According to the vulnerability assessment, these values would then "flow directly to database operations without sanitization."

The Missing Backend Protection

Compounding this issue, the Express backend in backend/api.js lacked CSRF middleware entirely. A properly configured Express application should include middleware like this:

const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.use(csrfProtection);

Without this protection, the application couldn't verify that requests were originating from legitimate user sessions rather than from malicious third-party sites.

Real-World Attack Scenario

Here's how an attacker could exploit this specific vulnerability in the booking application:

Step 1: Craft the Payload
The attacker creates a malicious coupon code containing SQL injection or script tags:

MALICIOUS'; UPDATE bookings SET price=0 WHERE user_id=123;--

Step 2: Submit Through the Form
The attacker (or a victim tricked by a CSRF attack) enters this value into the coupon input field in Listing.jsx. The toUpperCase() function runs, but the dangerous SQL syntax passes through untouched.

Step 3: Backend Processing
Without CSRF tokens to validate the request's origin and without input sanitization, the backend receives:

MALICIOUS'; UPDATE BOOKINGS SET PRICE=0 WHERE USER_ID=123;--

Step 4: Database Execution
If the backend uses string concatenation or unsafe query construction (as the vulnerability description suggests with "flow directly to database operations"), the SQL injection executes, potentially:
- Modifying booking prices to zero
- Accessing other users' booking data
- Deleting records
- Exfiltrating sensitive information

The same attack vector applies to the "user profile about sections" mentioned in the PR description, where HTML/script tags could be injected to perform stored XSS attacks against other users viewing the profile.

The Fix: Defense-in-Depth Input Validation

The security fix implements strict input validation at the frontend layer by adding a regex-based whitelist filter:

// BEFORE (vulnerable)
onChange={(e) => setCouponInput(e.target.value.toUpperCase())}

// AFTER (fixed)
onChange={(e) => setCouponInput(e.target.value.toUpperCase().replace(/[^A-Z0-9\-_]/g, ""))}

How This Fix Works

The regex pattern /[^A-Z0-9\-_]/g is a whitelist approach that:

  1. [^...] - Negated character class (matches anything NOT in the set)
  2. A-Z - Allows uppercase letters (combined with toUpperCase())
  3. 0-9 - Allows digits
  4. \- - Allows hyphens (escaped for clarity)
  5. _ - Allows underscores
  6. /g - Global flag (replaces all matches, not just the first)

The .replace() method removes any character that doesn't match this whitelist, effectively stripping out:
- SQL syntax characters: ', ;, --, /*, */
- HTML/script tags: <, >, /, =
- Special characters: $, {, }, (, ), [, ]
- Whitespace and control characters

Example Transformations

Here's what happens to malicious inputs with the fix in place:

// SQL Injection attempt
Input:  "SUMMER20'; DROP TABLE bookings;--"
Output: "SUMMER20DROPTABLEBOOKINGS"

// XSS attempt
Input:  "<script>alert('XSS')</script>"
Output: "SCRIPTALERTXSSSCRIPT"

// Template injection
Input:  "SAVE10${process.env.SECRET}"
Output: "SAVE10PROCESSENVSECRET"

// Legitimate coupon codes still work
Input:  "SUMMER-2024_SPECIAL"
Output: "SUMMER-2024_SPECIAL" 

Why This Defense Layer Matters

While the ultimate solution requires implementing CSRF middleware in the Express backend (which should still be done), this frontend fix provides immediate protection by:

  1. Preventing injection attacks at the source - Malicious characters never reach the backend
  2. Maintaining usability - Legitimate coupon codes like "SAVE20", "SUMMER-SALE", or "VIP_2024" still work
  3. Providing defense-in-depth - Even if backend validation is bypassed, the frontend acts as a gatekeeper
  4. Being immediately deployable - No backend changes or database migrations required

Key Takeaways

  • The Listing.jsx coupon input at line 133 accepted any characters, allowing SQL injection and XSS payloads to reach backend database operations before the fix
  • Combining toUpperCase() with a whitelist regex (/[^A-Z0-9\-_]/g) provides robust frontend input sanitization while maintaining usability for legitimate coupon codes
  • Missing CSRF middleware in backend/api.js meant the Express application couldn't distinguish legitimate user requests from forged cross-site attacks
  • Frontend validation alone is insufficient—this fix should be paired with backend CSRF token validation and parameterized database queries for complete protection
  • Whitelist validation is superior to blacklist approaches for input fields with predictable formats like coupon codes, promotional codes, or alphanumeric identifiers

How Orbis AppSec Detected This

  • Source: User-controlled input from the coupon code text field (couponInput state variable in Listing.jsx:133) and user profile about sections
  • Sink: Database operations in the backend that receive unsanitized coupon codes and profile data, combined with missing CSRF middleware validation in backend/api.js
  • Missing control: No input validation regex filter on the frontend onChange handler, no CSRF token validation middleware in the Express application, and insufficient backend sanitization before database queries
  • CWE: CWE-352 (Cross-Site Request Forgery) for missing CSRF protection, CWE-20 (Improper Input Validation) for unrestricted input acceptance
  • Fix: Added .replace(/[^A-Z0-9\-_]/g, "") to the onChange handler, creating a whitelist filter that strips all characters except uppercase letters, digits, hyphens, and underscores

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 how security gaps can exist at multiple layers of an application. The Express backend lacked CSRF middleware protection, while the React frontend's coupon input field in Listing.jsx failed to validate user input before sending it to backend operations. The implemented fix—adding a regex-based whitelist filter to strip dangerous characters—provides immediate protection against injection attacks while maintaining full functionality for legitimate coupon codes.

However, this frontend fix should be viewed as one layer of defense-in-depth. Complete security requires implementing CSRF token validation in the Express backend, using parameterized queries for all database operations, and validating input at every layer of the application. By combining these approaches, developers can build robust applications that resist both injection attacks and cross-site request forgery.

For developers working on similar Express-React applications, remember: every user input field is a potential attack vector. Validate early, validate often, and always implement proper CSRF protection for state-changing operations.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #30

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 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.

high

How CSRF and Missing Authentication Protection Happens in Node.js Express Routes and How to Fix It

A critical vulnerability in code-server's `/mint-key` endpoint allowed unauthenticated cross-origin requests to generate or retrieve VS Code web server authentication keys. By adding the `ensureAuthenticated` middleware to the POST handler, the fix ensures only authenticated users can mint new keys, eliminating the CSRF attack vector.

high

How CSRF vulnerabilities happen in Express.js and how to fix them

A HIGH severity Cross-Site Request Forgery (CSRF) vulnerability was found in `integrationExamples/topics/topics-server.js`, an Express.js demo server that lacked any CSRF protection middleware. The file was deleted entirely after assessment determined it posed unnecessary risk to downstream consumers of this Node.js library.

high

How Cross-Site Request Forgery (CSRF) happens in Express.js and how to fix it

A semgrep audit flagged `devboard/server/index.js` for lacking any CSRF middleware, meaning every state-changing route (`POST`, `PUT`, `DELETE` under `/api/*`) could be triggered by a forged cross-origin request riding on a victim's session cookie. The fix wires in `cookie-parser` and `csurf` right after body parsing, so every mutating request now requires a valid, per-session CSRF token before it reaches route handlers.

critical

How OAuth CSRF Attacks Happen in Node.js and How to Fix Them

A missing OAuth state parameter validation in `src/account_manager.js` left the `startOAuthServer()` function vulnerable to CSRF attacks, allowing an attacker to inject their own authorization code into a victim's active OAuth session. The fix generates a cryptographically random state token using `crypto.randomBytes()`, returns it alongside the server handle, and rejects any callback where the returned state doesn't match — closing the attack window entirely. This affects all downstream consume