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 Missing CSRF Middleware happens in Express.js and how to fix it

A high-severity CSRF vulnerability was discovered in `libProxy.js` of an Express.js application — the app had no CSRF middleware protecting its state-changing routes, leaving them open to cross-site request forgery attacks. The fix introduces a `csrf` token library, a `/csrf-token` endpoint to issue tokens, and a middleware that validates `x-csrf-token` headers or `_csrf` body fields on all non-safe HTTP methods. This proactive hardening removes an exploit primitive that could be chained with ot

high

How CSRF bypass happens in React Router RSC mode and how to fix it

A high-severity CSRF bypass vulnerability (GHSA-qwww-vcr4-c8h2) in React Router's RSC (React Server Components) mode allowed attackers to execute actions before the framework returned a 400 response. This vulnerability affected React Router versions prior to 7.18.2 and 8.3.0, enabling cross-site request forgery attacks that could bypass standard CSRF protections in applications using RSC mode.

high

How CSRF vulnerability happens in JavaScript fetch() calls and how to fix it

A high-severity CSRF vulnerability was discovered in Moodle's VvvebJs page builder where POST requests to `saveReusableUrl` and `saveUrl` endpoints lacked CSRF token validation. Without proper sesskey inclusion, attackers could trick authenticated users into executing unauthorized page modifications. The fix adds Moodle's sesskey token to both client-side fetch requests and enforces server-side validation with `require_sesskey()`.

high

How Regular Expression Denial of Service happens in JavaScript and how to fix it

CVE-2026-33671 is a Regular Expression Denial of Service (ReDoS) vulnerability in the picomatch glob-matching library, triggered by specially crafted extglob patterns that cause catastrophic regex backtracking. The fix upgrades picomatch to version 4.0.4 (with overrides pinning all transitive copies) in the client's dependency tree, eliminating the vulnerable regex evaluation path. Left unpatched, any code path that passes user-influenced glob patterns to picomatch could be weaponized to stall a

high

How insecure string copy functions happen in C and how to fix them

A high-severity buffer overflow risk was discovered in `login/main.c` where `strcpy()` was used to copy the `HOME` environment variable into a fixed-size 512-byte buffer without any bounds checking. An attacker controlling the `HOME` environment variable could overflow `pwd_file_name`, potentially corrupting memory or hijacking execution. The fix replaces the two-step `strcpy`/`strcat` pattern with a single, bounds-safe `snprintf` call.

high

How Denial of Service via Infinite Loop happens in JavaScript (nanoid) and how to fix it

A high-severity denial of service vulnerability (CVE-2026-67213) was discovered in nanoid versions before 5.1.6 and 3.3.18, where the `customAlphabet` function could enter an infinite loop during random ID generation. The fix upgrades the transitive nanoid dependency from 3.3.16 to 3.3.18 using pnpm overrides, ensuring the vulnerable code path is eliminated from the entire dependency tree including PostCSS.