security · web vulnerabilities · best practices

Backend Security Essentials:
SQL Injection, XSS, and CSRF Prevention

Protect your applications from the most common web attacks

❝ 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.

1. SQL Injection (SQLi)

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.

🔓 Vulnerable Example (Node.js + PostgreSQL)

// 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
});

🛡️ Secure Implementation: Parameterized Queries

// 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
});
Best Practices:
  • Always use parameterized queries / prepared statements (ORM like Sequelize, TypeORM, Prisma also prevent SQLi by default).
  • Validate and sanitize input – but never as a replacement for parameterization.
  • Use least privilege database accounts – avoid using superuser for app connections.
  • Stored procedures can also be safe if they use parameters correctly.

🛠️ ORM Example (Sequelize)

// 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.

2. Cross-Site Scripting (XSS)

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.

🔓 Vulnerable Example (Stored 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);
});

🛡️ Secure Implementation: Output Encoding & Content Security Policy

// 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);
});
XSS Prevention Strategies:
  • Context‑sensitive output encoding – HTML, JavaScript, URL, CSS contexts each require different encoding.
  • Use templating engines that auto‑escape (EJS, Handlebars, Pug) and never use | safe unless absolutely necessary.
  • Content Security Policy (CSP) – restrict sources of scripts and inline execution.
  • Sanitize user input – for rich text, use a library like DOMPurify (server‑side) to strip dangerous tags.
  • HttpOnly cookies – prevent JavaScript access to session cookies.

🌐 Setting a Strict CSP (Helmet.js)

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.

3. Cross-Site Request Forgery (CSRF)

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.

🔓 Vulnerable Example

<!-- 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.

🛡️ Prevention Methods

🎫 CSRF Tokens (Synchronizer Token Pattern)

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
});

🍪 SameSite Cookies

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'
}));
Additional Defenses:
  • Double‑Submit Cookie – send CSRF token both in a cookie and a request header, compare them server‑side.
  • Custom request headers (e.g., X-Requested-With: XMLHttpRequest) – not foolproof but adds a layer.
  • Verify origin/referer headers – but can be spoofed in some scenarios.
  • Re‑authentication for sensitive actions – e.g., prompt for password again before changing email.

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.

4. Security Headers: An Extra Layer

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'
    }
}));

5. Input Validation: The First Line of Defense

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.

6. Case Study: Securing a Fintech Application

A fintech startup had multiple vulnerabilities:

Result: after these changes, a penetration test found zero critical vulnerabilities, and the company passed their security audit with flying colors.

7. Backend Security Checklist

Final Thoughts: Security is a Mindset

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.