Security Engineering

Protecting Against Injection Attacks: SQL, NoSQL, and XSS

Prevent SQL injection, NoSQL injection, and XSS attacks with validated code patterns. Covers parameterized queries, input sanitization, and CSP configuration.

Khalid Aboubakr
16 min read
Sql InjectionXssNosql InjectionInput SanitizationSecurityCspParameterized Queries

Introduction

Injection attacks remain the most dangerous web application vulnerabilities. They occur when untrusted data is sent to an interpreter as part of a command or query.

SQL Injection Prevention

Parameterized Queries

// ❌ NEVER: String concatenation const query = `SELECT * FROM users WHERE email = '${email}'`; // ✅ ALWAYS: Parameterized queries // Using pg (node-postgres) const result = await pool.query( 'SELECT * FROM users WHERE email = $1', [email] ); // Using Prisma const user = await prisma.user.findUnique({ where: { email }, }); // Using Knex const users = await knex('users') .where('email', email) .first(); // Using TypeORM const user = await userRepository.findOne({ where: { email }, });

Dynamic Query Building

// Safe dynamic query building with Knex function buildSearchQuery(filters: SearchFilters) { let query = knex('products').select('*'); if (filters.category) { query = query.where('category', filters.category); } if (filters.minPrice !== undefined) { query = query.where('price', '>=', filters.minPrice); } if (filters.maxPrice !== undefined) { query = query.where('price', '<=', filters.maxPrice); } if (filters.search) { // Safe full-text search query = query.whereRaw( 'to_tsvector(name || \' \' || description) @@ plainto_tsquery(?)', [filters.search] ); } // Safe dynamic ordering const allowedSortFields = ['name', 'price', 'created_at']; if (filters.sortBy && allowedSortFields.includes(filters.sortBy)) { query = query.orderBy(filters.sortBy, filters.sortOrder === 'desc' ? 'desc' : 'asc'); } return query; }

NoSQL Injection Prevention

// ❌ Vulnerable to NoSQL injection const user = await db.collection('users').findOne({ username: req.body.username, password: req.body.password, // Attacker can send { "$gt": "" } }); // ✅ Validate and sanitize input types import { z } from 'zod'; const loginSchema = z.object({ username: z.string().min(1).max(50), password: z.string().min(1).max(100), }); const { username, password } = loginSchema.parse(req.body); // Now safe - values are guaranteed to be strings const user = await db.collection('users').findOne({ username, password: await bcrypt.hash(password, hashedPassword), }); // ✅ Use MongoDB's strict query operators const user = await db.collection('users').findOne({ username: { $eq: username }, // Explicit equality });

XSS Prevention

Output Encoding

import DOMPurify from 'isomorphic-dompurify'; import { encode } from 'html-entities'; // For plain text output function escapeHtml(text: string): string { return encode(text); } // For rich text that needs some HTML function sanitizeHtml(html: string): string { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], ALLOWED_ATTR: ['href', 'title'], ALLOW_DATA_ATTR: false, }); } // React automatically escapes by default function UserProfile({ user }: { user: User }) { return ( <div> {/* Safe - React escapes this */} <h1>{user.name}</h1> {/* ❌ Dangerous - bypasses React's escaping */} <div dangerouslySetInnerHTML={{ __html: user.bio }} /> {/* ✅ Safe - sanitize first */} <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(user.bio) }} /> </div> ); }

Content Security Policy

// Strict CSP that prevents inline scripts app.use((req, res, next) => { const nonce = crypto.randomBytes(16).toString('base64'); res.locals.nonce = nonce; res.setHeader('Content-Security-Policy', [ "default-src 'self'", `script-src 'self' 'nonce-${nonce}'`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "connect-src 'self' https://api.example.com", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", ].join('; ')); next(); });

Command Injection Prevention

import { exec, execFile } from 'child_process'; // ❌ NEVER: Shell command with user input exec(`convert ${userFilename} output.png`); // ✅ Use execFile with arguments array execFile('convert', [userFilename, 'output.png'], (error, stdout) => { // Safe - arguments are not interpreted by shell }); // ✅ Better: Avoid shell entirely import sharp from 'sharp'; await sharp(userFilename) .resize(800, 600) .toFile('output.png');

Conclusion

Injection prevention requires:

  1. Never trust user input - validate and sanitize everything
  2. Use parameterized queries - never concatenate SQL
  3. Validate data types - especially for NoSQL
  4. Encode output - context-appropriate encoding
  5. Implement CSP - defense in depth against XSS
  6. Avoid shell commands - use libraries instead

Defense in depth is key - multiple layers of protection ensure that if one fails, others still protect you.

Related Articles

Security Engineering18 min read

API Security Hardening: A Practitioner's Guide

Secure your APIs with rate limiting, input validation, and CORS configuration. Production-tested checklist covering authentication, encryption, and error handling.

Security Engineering15 min read

Secure Session Management: Patterns and Pitfalls

Implement secure session management with proper cookie settings, token rotation, and logout flows. Covers session fixation, hijacking prevention, and multi-device handling.

Backend Design18 min read

Laravel at Scale: Enterprise Patterns Beyond MVC

Build enterprise Laravel applications with repository pattern, service layer, and DDD principles. Production patterns from government and healthcare systems.

Backend Design20 min read

Database Design Patterns for Scale

Scale databases with sharding, replication, and partitioning. Covers PostgreSQL, MySQL, and MongoDB scaling patterns with real performance numbers from production systems.