Introduction
As web developers, we often focus on functionality, performance, and user experience. However, security should be a fundamental consideration from the start of any project. A single security vulnerability can lead to data breaches, financial losses, and damaged reputation.
This guide covers essential secure coding practices that every web developer should implement to build robust and secure applications.
Authentication & Authorization
Secure Password Storage
Never store passwords in plain text. Always use strong hashing algorithms with salt.
// ❌ BAD: Plain text storage
const password = 'user123';
await db.users.create({ password: password });
// ✅ GOOD: Using bcrypt with salt
import bcrypt from 'bcrypt';
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
await db.users.create({ password: hashedPassword });
// Verifying password
const isValid = await bcrypt.compare(inputPassword, hashedPassword);JWT Best Practices
When using JSON Web Tokens for authentication:
// ✅ GOOD: Secure JWT implementation
import jwt from 'jsonwebtoken';
// Use strong secret (store in environment variables)
const secret = process.env.JWT_SECRET; // At least 256 bits
// Set appropriate expiration
const token = jwt.sign(
{ userId: user.id },
secret,
{ expiresIn: '1h' } // Short expiration time
);
// Store refresh tokens separately
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);Important JWT security tips:
- Never store sensitive data in JWT payload
- Use HTTPS only
- Implement token rotation
- Store tokens in httpOnly cookies, not localStorage
Input Validation & Sanitization
Preventing XSS (Cross-Site Scripting)
Always sanitize user input before rendering:
// ❌ BAD: Directly rendering user input
function UserProfile({ bio }) {
return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}
// ✅ GOOD: Sanitize before rendering
import DOMPurify from 'dompurify';
function UserProfile({ bio }) {
const sanitizedBio = DOMPurify.sanitize(bio);
return <div dangerouslySetInnerHTML={{ __html: sanitizedBio }} />;
}
// ✅ BETTER: Use React's built-in escaping
function UserProfile({ bio }) {
return <div>{bio}</div>; // React automatically escapes
}SQL Injection Prevention
Use parameterized queries or ORMs:
// ❌ BAD: String concatenation
const query = `SELECT * FROM users WHERE email = '${userEmail}'`;
db.execute(query);
// ✅ GOOD: Parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [userEmail]);
// ✅ BETTER: Using ORM (Prisma example)
const user = await prisma.user.findUnique({
where: { email: userEmail }
});API Security
Rate Limiting
Protect your APIs from abuse:
// Using express-rate-limit
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later.',
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', limiter);CORS Configuration
Configure CORS properly:
// ❌ BAD: Allowing all origins
app.use(cors({ origin: '*' }));
// ✅ GOOD: Whitelist specific origins
const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com'];
app.use(
cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
})
);API Key Management
// ✅ GOOD: Secure API key handling
// Never expose API keys in client-side code
// Use environment variables
const apiKey = process.env.API_KEY;
// For server-to-server communication only
const response = await fetch('https://api.service.com/data', {
headers: {
Authorization: `Bearer ${apiKey}`
}
});Data Protection
Encryption
// ✅ GOOD: 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'),
encryptedData: encrypted,
authTag: authTag.toString('hex')
};
}
function decrypt(encrypted) {
const decipher = crypto.createDecipheriv(
algorithm,
key,
Buffer.from(encrypted.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
let decrypted = decipher.update(encrypted.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}Environment Variables
// ❌ BAD: Hardcoded secrets
const dbPassword = "mySecretPassword123";
const apiKey = "sk-1234567890abcdef";
// ✅ GOOD: Using environment variables
// .env file (add to .gitignore!)
DB_PASSWORD=mySecretPassword123
API_KEY=sk-1234567890abcdef
JWT_SECRET=your-super-secret-key
// In your code
const dbPassword = process.env.DB_PASSWORD;
const apiKey = process.env.API_KEY;Security Headers
Implement essential security headers:
// Using helmet.js in Express
import helmet from 'helmet';
app.use(helmet());
// Or manually configure headers
app.use((req, res, next) => {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Enable XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// HTTPS only
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
// Content Security Policy
res.setHeader('Content-Security-Policy', "default-src 'self'");
next();
});Common OWASP Vulnerabilities
1. Broken Access Control
// ❌ BAD: No authorization check
app.delete('/api/users/:id', async (req, res) => {
await db.users.delete(req.params.id);
res.json({ success: true });
});
// ✅ GOOD: Verify user permissions
app.delete('/api/users/:id', authenticateUser, async (req, res) => {
// Check if user is deleting their own account or is admin
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
await db.users.delete(req.params.id);
res.json({ success: true });
});2. Cryptographic Failures
// ❌ BAD: Using weak encryption
const hash = crypto.createHash('md5').update(password).digest('hex');
// ✅ GOOD: Using strong algorithms
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 10);3. Injection Attacks
Already covered in SQL Injection section, but also applies to:
- Command Injection
- LDAP Injection
- XML Injection
General rule: Never trust user input. Always validate and sanitize.
Security Checklist
Before deploying your application, ensure:
- [ ] All passwords are hashed with bcrypt or argon2
- [ ] Input validation on both client and server
- [ ] Parameterized queries for database operations
- [ ] HTTPS enabled (redirect HTTP to HTTPS)
- [ ] Security headers configured
- [ ] CORS properly configured
- [ ] Rate limiting implemented
- [ ] Secrets stored in environment variables
- [ ] Dependencies regularly updated
- [ ] Error messages don't expose sensitive info
- [ ] Authentication & authorization properly implemented
- [ ] File uploads validated and sanitized
- [ ] Logging and monitoring in place
Tools & Resources
Security Testing Tools:
- OWASP ZAP - Web application security scanner
- Burp Suite - Security testing platform
- npm audit - Check for vulnerable dependencies
- Snyk - Continuous security monitoring
Learning Resources:
Conclusion
Security is not a one-time task but an ongoing process. By implementing these secure coding practices from the start, you significantly reduce the risk of vulnerabilities in your applications.
Remember:
- Defense in depth - Multiple layers of security
- Principle of least privilege - Grant minimum necessary permissions
- Never trust user input - Always validate and sanitize
- Keep dependencies updated - Regularly patch vulnerabilities
- Security is everyone's responsibility - Not just the security team
Stay secure, and happy coding! 🔒


