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:
[^...]- Negated character class (matches anything NOT in the set)A-Z- Allows uppercase letters (combined withtoUpperCase())0-9- Allows digits\-- Allows hyphens (escaped for clarity)_- Allows underscores/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:
- Preventing injection attacks at the source - Malicious characters never reach the backend
- Maintaining usability - Legitimate coupon codes like "SAVE20", "SUMMER-SALE", or "VIP_2024" still work
- Providing defense-in-depth - Even if backend validation is bypassed, the frontend acts as a gatekeeper
- 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.jsxcoupon 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.jsmeant 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 (
couponInputstate variable inListing.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
onChangehandler, 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 theonChangehandler, 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
- CWE-352: Cross-Site Request Forgery (CSRF)
- CWE-20: Improper Input Validation
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- OWASP Input Validation Cheat Sheet
- Express.js CSRF Protection with csurf
- Semgrep Rules for Express Security
- fix: user-controlled input fields (coupon codes, use... in Listing.jsx