Overview
Injection vulnerabilities in Node.js with Express arise when user-supplied data is integrated directly into database queries, shell commands, or template engines. In real-world apps, this often happens when building SQL strings with string concatenation, or when constructing NoSQL queries from request data without validation. The impact can include data leakage, unauthorized updates, authentication bypass, and, in the worst cases, remote code execution if an attacker can run arbitrary commands on the server.
Attack patterns in Express typically involve routes that read input from req.query or req.body and embed it into vulnerable code paths. Examples include raw SQL built with string interpolation, NoSQL queries that concatenate user input into the query, or OS command execution with child_process.exec or spawn using unchecked input. These patterns are common because developers rely on simple string concatenation for convenience, forgetting that the same input could alter logic or access controls.
Mitigation involves embracing parameterized queries and safe abstractions. Use prepared statements via database drivers or ORMs (for SQL databases) and ensure you use placeholder parameters for all user-supplied values. For NoSQL databases, build queries using operators rather than string building and apply strict schema validation. Sanitize and validate input against allowlists, avoid using eval or untrusted template features, and avoid invoking shell commands with user data. Implement proper error handling to avoid leaking details and enable auditing to detect injection attempts.
Code Fix Example
Node.js (Express) API Security Remediation
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json());
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: '',
database: 'shop',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Vulnerable pattern: string concatenation
app.get('/products/search', async (req, res) => {
const q = req.query.q || '';
try {
const [rows] = await pool.query(`SELECT * FROM products WHERE name = '${q}'`);
res.json(rows);
} catch (err) {
res.status(500).send('Error');
}
});
// Fixed pattern: parameterized query
app.get('/products/search-secure', async (req, res) => {
const q = req.query.q || '';
try {
const [rows] = await pool.execute('SELECT * FROM products WHERE name = ?', [q]);
res.json(rows);
} catch (err) {
res.status(500).send('Error');
}
});
app.listen(3000, () => console.log('Server listening on port 3000'));