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

Prevention & Best Practices

1. Always Implement CSRF Protection in Express

For the backend, add CSRF middleware to your Express application:

const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

const app = express();
const csrfProtection = csrf({ cookie: true });

app.use(cookieParser());
app.use(csrfProtection);

// In your routes
app.get('/booking', (req, res) => {
  res.render('booking', { csrfToken: req.csrfToken() });
});

app.post('/apply-coupon', (req, res) => {
  // CSRF token is automatically validated
  // Process coupon code
});

Include the CSRF token in your React frontend:

const [csrfToken, setCsrfToken] = useState('');

useEffect(() => {
  fetch('/api/csrf-token')
    .then(r => r.json())
    .then(data => setCsrfToken(data.token));
}, []);

// Include in requests
fetch('/api/apply-coupon', {
  method: 'POST',
  headers: {
    'CSRF-Token': csrfToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ coupon: couponInput })
});

2. Validate Input on Multiple Layers

Implement validation at three levels:

Frontend (immediate feedback):

const isValidCoupon = (code) => /^[A-Z0-9\-_]{4,20}$/.test(code);

onChange={(e) => {
  const sanitized = e.target.value.toUpperCase().replace(/[^A-Z0-9\-_]/g, "");
  setCouponInput(sanitized);
  setIsValid(isValidCoupon(sanitized));
}}

Backend API (security boundary):

app.post('/apply-coupon', csrfProtection, (req, res) => {
  const { coupon } = req.body;

  // Validate format
  if (!/^[A-Z0-9\-_]{4,20}$/.test(coupon)) {
    return res.status(400).json({ error: 'Invalid coupon format' });
  }

  // Use parameterized queries
  db.query('SELECT * FROM coupons WHERE code = ?', [coupon], (err, results) => {
    // Process results
  });
});

Database (last line of defense):

CREATE TABLE coupons (
  code VARCHAR(20) CHECK (code ~ '^[A-Z0-9\-_]+$'),
  -- other columns
);

3. Use Parameterized Queries

Never concatenate user input into SQL queries:

// VULNERABLE
const query = `SELECT * FROM coupons WHERE code = '${couponCode}'`;

// SAFE
const query = 'SELECT * FROM coupons WHERE code = ?';
db.query(query, [couponCode], callback);

4. Implement Content Security Policy

Add CSP headers to mitigate XSS even if injection occurs:

app.use((req, res, next) => {
  res.setHeader("Content-Security-Policy", 
    "default-src 'self'; script-src 'self'; object-src 'none'");
  next();
});

5. Use Security Linters

Configure ESLint with security plugins:

{
  "plugins": ["security"],
  "extends": ["plugin:security/recommended"],
  "rules": {
    "security/detect-unsafe-regex": "error",
    "security/detect-non-literal-regexp": "warn"
  }
}

6. Regular Security Audits

Use tools to detect missing CSRF protection:

  • Semgrep: Scan for missing CSRF middleware patterns
  • npm audit: Check for vulnerable dependencies
  • OWASP ZAP: Test for CSRF vulnerabilities in running applications

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.

References

Frequently Asked Questions

What is CSRF protection in Express.js?

CSRF (Cross-Site Request Forgery) protection in Express.js involves middleware like `csurf` or `csrf` that generates and validates unique tokens for each user session, preventing unauthorized requests from malicious websites. Without this middleware, attackers can trick authenticated users into executing unwanted actions.

How do you prevent CSRF attacks in Express.js applications?

Implement CSRF middleware like `csurf` in your Express application, generate unique tokens for each session, include tokens in forms or headers, validate tokens on state-changing requests, and combine with SameSite cookie attributes. Additionally, validate and sanitize all user inputs before processing.

What CWE is CSRF vulnerability?

CSRF vulnerability is classified as CWE-352 (Cross-Site Request Forgery). When combined with insufficient input validation as in this case, it also involves CWE-20 (Improper Input Validation), creating a compound vulnerability that allows injection attacks.

Is input validation enough to prevent CSRF attacks?

No, input validation alone is insufficient for CSRF protection. While it prevents injection attacks through validated fields, CSRF attacks exploit authenticated sessions to perform unauthorized actions. You need both CSRF tokens (to verify request origin) and input validation (to prevent injection attacks).

Can static analysis detect CSRF vulnerabilities?

Yes, static analysis tools like Semgrep can detect missing CSRF middleware in Express applications by analyzing route handlers and middleware chains. They flag applications lacking CSRF protection libraries and can identify user input flows that bypass validation, as demonstrated in this vulnerability detection.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #30

Related Articles

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.

high

How Missing CSRF Middleware Happens in Express.js and How to Fix It

A high-severity CSRF vulnerability was discovered in `backend/server.js` of an Express.js application — the server had no CSRF middleware protecting state-changing routes. Without CSRF protection, attackers could trick authenticated users into unknowingly submitting forged requests (like transferring funds or changing account details). The fix adds the `csurf` package with cookie-based token validation, closing the attack surface with a few targeted lines of code.

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

critical

How CSRF Protection Failures Happen in FastAPI and How to Fix Them

A critical CORS misconfiguration in `backend/main.py` allowed cookies to be sent alongside wildcard-origin requests, violating the CORS specification and opening the door to cross-site request forgery attacks. The fix conditionally disables `allow_credentials` when the allowed origins list contains a wildcard, bringing the configuration into compliance with browser security rules. This change closes a subtle but dangerous gap that could have let attackers on sibling subdomains forge authenticate