❝ SQL Injection, Cross‑Site Scripting (XSS), and Cross‑Site Request Forgery (CSRF) are three of the most critical security vulnerabilities facing web applications. They consistently top the OWASP Top 10 list, and their exploitation can lead to data breaches, account takeovers, and complete system compromise. Understanding how these attacks work and implementing robust defenses is non‑negotiable for any backend developer.❞
This guide explains each vulnerability in depth, with real‑world code examples (both vulnerable and secure versions), and provides practical, layered defenses. You'll learn how to use parameterized queries, output encoding, CSRF tokens, and modern security headers to build resilient applications.
SQL Injection occurs when untrusted data is concatenated into SQL queries, allowing attackers to alter the query structure and execute arbitrary SQL commands. This can lead to data theft, deletion, or even remote code execution.
// DO NOT DO THIS
const userId = req.query.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
pool.query(query, (err, result) => {
// attacker can pass: id=1 OR 1=1; --
// This would return all users
});
// Use parameterized queries (prepared statements)
const userId = req.query.id;
const query = 'SELECT * FROM users WHERE id = $1';
pool.query(query, [userId], (err, result) => {
// SQL engine treats userId as data, not code
});
// Sequelize automatically escapes input
const user = await User.findOne({
where: { id: req.query.id }
});
SQL Injection remains one of the most dangerous vulnerabilities because it can give attackers complete control over your database. Preventing it is straightforward: never concatenate user input into SQL strings.
XSS allows attackers to inject malicious scripts into web pages viewed by other users. The injected script can steal cookies, session tokens, or perform actions on behalf of the victim. There are three main types: Stored XSS, Reflected XSS, and DOM‑based XSS.
// User submits a comment with malicious script
// POST /comments with body: { text: "<script>alert('XSS')</script>" }
app.post('/comments', (req, res) => {
const comment = req.body.text;
// Store directly in database (no sanitization)
db.query('INSERT INTO comments (text) VALUES ($1)', [comment]);
res.send('OK');
});
// Later, when displaying comments:
app.get('/comments', async (req, res) => {
const comments = await db.query('SELECT text FROM comments');
// Directly rendering unsanitized content
let html = '<ul>' + comments.rows.map(c => `<li>${c.text}</li>`).join('') + '</ul>';
res.send(html);
});
// Backend: store raw input (but escape on output)
// Using a templating engine like EJS with automatic escaping
// Or manual escaping with a library like 'escape-html'
const escapeHtml = require('escape-html');
app.get('/comments', async (req, res) => {
const comments = await db.query('SELECT text FROM comments');
// Escape each comment before rendering
let html = '<ul>' + comments.rows.map(c => `<li>${escapeHtml(c.text)}</li>`).join('') + '</ul>';
res.send(html);
});
| safe unless absolutely necessary.const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
}
}));
XSS is a client‑side vulnerability, but the backend plays a crucial role by properly encoding output and setting security headers. Always assume any user‑generated content is dangerous until proven safe.
CSRF tricks an authenticated user into unknowingly submitting a request to a web application where they are currently logged in. For example, a malicious site could make the user's browser send a request to your bank's transfer endpoint using their active session cookies.
<!-- Malicious site -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
If the user is logged into bank.com, their browser will send the session cookie with the request, and the transfer will be executed.
Generate a unique, unpredictable token for each user session or request, and validate it on state‑changing requests.
// Using csurf middleware for Express
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('send', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// Token automatically validated
// Process transfer
});Set SameSite=Lax or Strict on session cookies. This prevents cookies from being sent with cross‑site requests.
// In Express with cookie-session or express-session
app.use(session({
secret: 'secret',
cookie: { sameSite: 'lax' } // or 'strict'
}));X-Requested-With: XMLHttpRequest) – not foolproof but adds a layer.CSRF is often mitigated by modern browsers via SameSite cookies, but you should still implement token‑based protection, especially for legacy browser support and critical actions.
Beyond the three vulnerabilities, adding security headers can protect against a range of attacks. Use Helmet.js in Express to easily set them.
const helmet = require('helmet');
app.use(helmet());
// Helmet sets:
// - X-Content-Type-Options: nosniff
// - X-Frame-Options: SAMEORIGIN
// - X-XSS-Protection: 1; mode=block
// - Strict-Transport-Security
// - Content-Security-Policy (if configured)
Additionally, set HttpOnly and Secure flags on cookies:
app.use(session({
cookie: {
httpOnly: true,
secure: true, // only over HTTPS
sameSite: 'lax'
}
}));
Validate all incoming data against expected types, lengths, and formats. Use libraries like Joi, express-validator, or zod.
const { body, validationResult } = require('express-validator');
app.post('/user',
body('email').isEmail().normalizeEmail(),
body('age').isInt({ min: 0, max: 120 }).toInt(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Proceed
});
Validation prevents malformed data from reaching your business logic and can block many injection attempts.
A fintech startup had multiple vulnerabilities:
escape-html and added a strict CSP that blocked inline scripts.csurf middleware and set SameSite=Lax on cookies.Result: after these changes, a penetration test found zero critical vulnerabilities, and the company passed their security audit with flying colors.
SQL injection, XSS, and CSRF have been known for decades, yet they still plague modern applications. The good news is that preventing them is well‑understood and straightforward with modern frameworks and libraries. By adopting a defense‑in‑depth strategy—parameterized queries, output encoding, CSRF tokens, security headers, and input validation—you can drastically reduce your attack surface. Make security part of your development lifecycle, not an afterthought. Your users' data and trust depend on it.
Build securely, ship confidently.