- Published on
Web Security Best Practices Every Developer Must Know
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Security vulnerabilities can destroy your app, your users' data, and your reputation overnight. In 2026, with AI-powered attacks growing more sophisticated, secure coding is a non-negotiable skill.
This guide covers the OWASP Top 10 threats and exactly how to prevent them.
- 1. SQL Injection — The Most Dangerous Vulnerability
- 2. Cross-Site Scripting (XSS)
- 3. Authentication and Password Storage
- 4. CSRF (Cross-Site Request Forgery)
- 5. Input Validation
- 6. Rate Limiting — Prevent Brute Force
- 7. Secure HTTP Headers with Helmet
- 8. Secrets Management
- Security Checklist
- Conclusion
1. SQL Injection — The Most Dangerous Vulnerability
SQL injection happens when user input is directly embedded into SQL queries:
// ❌ VULNERABLE — Never do this!
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`
// If email = "'; DROP TABLE users; --" → disaster!
// ✅ SAFE — Use parameterized queries
const user = await db.query(
'SELECT * FROM users WHERE email = $1',
[req.body.email] // Input is safely escaped
)
// ✅ SAFE with ORM (Prisma/Sequelize/TypeORM)
const user = await prisma.user.findUnique({
where: { email: req.body.email } // Automatically safe
})
Rule: Never concatenate user input into SQL. Always use parameterized queries or an ORM.
2. Cross-Site Scripting (XSS)
XSS occurs when malicious scripts are injected into web pages viewed by others:
// ❌ VULNERABLE — Renders user input as HTML
element.innerHTML = userInput
document.write(userInput)
// ✅ SAFE — Text only, no HTML interpretation
element.textContent = userInput
// ✅ SAFE in React — JSX escapes by default
return <div>{userInput}</div> // Safe!
return <div dangerouslySetInnerHTML={{ __html: userInput }} /> // ❌ Dangerous!
Content Security Policy (CSP) adds another layer of defense:
// In Express
import helmet from 'helmet'
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-{RANDOM}'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
}
}
}))
3. Authentication and Password Storage
import bcrypt from 'bcryptjs'
// ❌ NEVER store plain text passwords!
const user = { password: req.body.password } // Terrible!
// ✅ Always hash passwords with bcrypt
const SALT_ROUNDS = 12
async function register(email, password) {
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS)
await db.user.create({ email, password: hashedPassword })
}
async function login(email, password) {
const user = await db.user.findByEmail(email)
if (!user) return null // Don't reveal if user exists!
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) return null
return generateJWT(user)
}
JWT Best Practices:
import jwt from 'jsonwebtoken'
// ❌ INSECURE — Weak secret
const token = jwt.sign({ userId: 1 }, 'secret')
// ✅ SECURE
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET, // Long, random, from env
{
expiresIn: '15m', // Short-lived access token
issuer: 'my-api',
audience: 'my-client',
}
)
4. CSRF (Cross-Site Request Forgery)
CSRF tricks users into performing actions they didn't intend:
// Protection with CSRF tokens
import csrf from 'csurf'
app.use(csrf({ cookie: true }))
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() })
})
// In your form HTML
// <input type="hidden" name="_csrf" value="<%= csrfToken %>">
For APIs using JWT in Authorization headers, CSRF is not a concern — only cookie-based auth needs CSRF protection.
5. Input Validation
Never trust user input — always validate on the server:
import { z } from 'zod'
const RegisterSchema = z.object({
email: z.string().email().max(255),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[0-9]/, 'Must contain number'),
name: z.string().min(1).max(50).trim(),
})
app.post('/register', async (req, res) => {
const result = RegisterSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({
errors: result.error.flatten().fieldErrors
})
}
await createUser(result.data)
})
6. Rate Limiting — Prevent Brute Force
import rateLimit from 'express-rate-limit'
// Strict limit for auth routes
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Max 10 attempts
message: 'Too many login attempts. Try again in 15 minutes.',
standardHeaders: true,
})
// General API limit
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100,
})
app.use('/api/auth', authLimiter)
app.use('/api/', apiLimiter)
7. Secure HTTP Headers with Helmet
import helmet from 'helmet'
// Helmet sets a dozen security headers automatically
app.use(helmet())
// What Helmet sets:
// X-Frame-Options: DENY (prevents clickjacking)
// X-Content-Type-Options: nosniff (prevents MIME sniffing)
// Strict-Transport-Security (forces HTTPS)
// X-XSS-Protection: 0 (modern browsers don't need this)
// Referrer-Policy: no-referrer
// And more...
8. Secrets Management
// ❌ Never hardcode secrets in code
const SECRET = 'my-super-secret-key-123'
// ❌ Never commit .env files to Git
// ✅ Use environment variables
const SECRET = process.env.JWT_SECRET
// ✅ Validate required env vars on startup
const requiredEnvVars = ['JWT_SECRET', 'DATABASE_URL', 'REDIS_URL']
requiredEnvVars.forEach(varName => {
if (!process.env[varName]) {
throw new Error(`Missing required env variable: ${varName}`)
}
})
Security Checklist
Before deploying any app, verify:
- ✅ Parameterized queries (no SQL injection)
- ✅ Passwords hashed with bcrypt (cost factor ≥ 10)
- ✅ HTTPS everywhere (use Let's Encrypt)
- ✅ Helmet.js for security headers
- ✅ Input validation on all endpoints
- ✅ Rate limiting on auth routes
- ✅ Secrets in environment variables, never in code
- ✅ Dependencies audited (
npm audit) - ✅ Error messages don't reveal internal details
- ✅ JWT expiry is short (15min access, 7d refresh)
Conclusion
Security isn't a feature you add at the end — it's a discipline you practice from day one. Use parameterized queries, hash passwords, validate all input, add rate limiting, and scan your dependencies regularly. One security breach can undo years of work. The cost of prevention is minimal compared to the cost of a breach.