Secure Coding Best Practices: The Complete Developer Guide
Introduction
Security vulnerabilities in software are primarily caused by coding errors. Studies show that 80% of security incidents stem from preventable defects in code. The solution is not to add security on top of your code, but to write secure code from the start.
This comprehensive guide covers essential secure coding practices every developer should know.
The Security Mindset
Before diving into specific practices, adopt these fundamental security principles:
┌─────────────────────────────────────────────────────────────┐
│ CORE SECURITY PRINCIPLES │
├─────────────────────────────────────────────────────────────┤
│ 1. DEFENSE IN DEPTH │
│ Layer your defenses - no single layer is foolproof │
├─────────────────────────────────────────────────────────────┤
│ 2. FAIL SECURELY │
│ When something goes wrong, deny access by default │
├─────────────────────────────────────────────────────────────┤
│ 3. LEAST PRIVILEGE │
│ Grant minimum necessary permissions │
├─────────────────────────────────────────────────────────────┤
│ 4. ASSUME NOTHING │
│ Trust no input, validate everything │
├─────────────────────────────────────────────────────────────┤
│ 5. SECURE BY DEFAULT │
│ Make secure behavior the default option │
└─────────────────────────────────────────────────────────────┘
1. Input Validation
The Golden Rule
Never trust user input. Any data that enters your application from an external source must be validated.
// BAD - No validation
function createUser(userData) {
db.query(`INSERT INTO users (name, email) VALUES ('${userData.name}', '${userData.email}')`);
}
// GOOD - Validation first
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(1).max(100).pattern(/^[a-zA-Z\s]+$/),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150)
});
function createUser(userData) {
const { error, value } = userSchema.validate(userData);
if (error) {
throw new ValidationError(error.message);
}
db.query('INSERT INTO users (name, email) VALUES (?, ?)', [value.name, value.email]);
}
Validation Checklist
- [ ] Validate all inputs (forms, API parameters, headers, URLs)
- [ ] Define allowlists over denylists
- [ ] Validate data types, ranges, formats
- [ ] Sanitize output for display
- [ ] Validate file uploads (type, size, name)
- [ ] Use parameterized queries (never string concatenation)
2. Authentication
Password Handling
// GOOD - Use bcrypt with proper configuration
const bcrypt = require('bcrypt');
async function hashPassword(password) {
const saltRounds = 12; // Higher = more secure but slower
return await bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// Enforce password complexity
function validatePassword(password) {
const minLength = 12;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*]/.test(password);
if (password.length < minLength) return 'Too short';
if (!hasUpperCase || !hasLowerCase) return 'Need uppercase and lowercase';
if (!hasNumbers) return 'Need a number';
if (!hasSpecialChar) return 'Need a special character';
return null; // Valid
}
Session Management
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET, // Use a strong, unique secret
name: 'sessionId', // Don't use default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Prevent XSS access
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
}
}));
// Regenerate session after login
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
if (user) {
req.session.regenerate((err) => {
req.session.userId = user.id;
res.redirect('/dashboard');
});
}
});
Multi-Factor Authentication (MFA)
// Implement TOTP-based MFA
const speakeasy = require('speakeasy');
// Generate secret for user
function generateMfaSecret(userId) {
const secret = speakeasy.generateSecret({
name: `DevSecure:${userId}`,
issuer: 'DevSecure'
});
// Store secret in database (encrypted)
return secret;
}
// Verify token
function verifyMfa(token, secret) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 1 // Allow 1 step either side
});
}
3. Authorization
Role-Based Access Control (RBAC)
// Define permissions
const PERMISSIONS = {
admin: ['read', 'write', 'delete', 'admin'],
editor: ['read', 'write'],
viewer: ['read']
};
// Middleware for checking permissions
function requirePermission(permission) {
return (req, res, next) => {
const userRole = req.user?.role || 'guest';
const permissions = PERMISSIONS[userRole] || [];
if (!permissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Apply to routes
app.get('/admin/users', requirePermission('admin'), adminController.list);
app.get('/posts', requirePermission('read'), postController.list);
app.post('/posts', requirePermission('write'), postController.create);
Object-Level Authorization
// Check if user can access specific resource
async function canAccessResource(userId, resourceId) {
const user = await getUser(userId);
const resource = await getResource(resourceId);
// Admin can access everything
if (user.role === 'admin') return true;
// Users can only access their own resources
if (resource.ownerId === userId) return true;
return false;
}
// Use in every endpoint that accesses data by ID
app.get('/documents/:id', async (req, res) => {
const hasAccess = await canAccessResource(req.user.id, req.params.id);
if (!hasAccess) {
return res.status(403).json({ error: 'Access denied' });
}
const doc = await getDocument(req.params.id);
res.json(doc);
});
4. SQL Injection Prevention
Parameterized Queries
// BAD - Vulnerable to SQL injection
app.get('/users', (req, res) => {
const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;
db.query(query);
});
// GOOD - Parameterized query
app.get('/users', (req, res) => {
const query = 'SELECT * FROM users WHERE name = ?';
db.query(query, [req.query.name]);
});
// GOOD - Using an ORM
const { User } = require('./models');
const users = await User.findAll({
where: { name: req.query.name }
});
ORM Security Best Practices
// Don't use raw queries with user input
// BAD
await db.query(`SELECT * FROM ${tableName} WHERE id = ${userId}`);
// GOOD - Use parameterization or ORM
// ORM
await User.findById(userId);
// If you must use raw queries
await db.query('SELECT * FROM users WHERE id = ?', [userId]);
5. Cross-Site Scripting (XSS) Prevention
Output Encoding
// Use a sanitizer library
const createDomPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDomPurify(window);
// Sanitize before rendering
function sanitizeInput(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: ['href', 'class']
});
}
// In templates
const sanitizedContent = sanitizeInput(userInput);
element.innerHTML = sanitizedContent;
Content Security Policy
// Set CSP headers
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'nonce-{nonce}'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-ancestors 'none';"
);
next();
});
6. Cross-Site Request Forgery (CSRF) Prevention
CSRF Tokens
const csrf = require('csurf');
app.use(csrf({ cookie: true }));
// In your forms
function csrfToken(req) {
return req.csrfToken();
}
// Use in HTML form
<form action="/submit" method="POST">
<input type="hidden" name="_csrf" value="{csrfToken}" />
<!-- other fields -->
</form>
Same-Site Cookies
// Use SameSite attribute
app.use(session({
cookie: {
sameSite: 'strict', // 'strict' or 'lax'
secure: true,
httpOnly: true
}
}));
7. Secure File Handling
const multer = require('multer');
const path = require('path');
const upload = multer({
// Limit file size to 5MB
limits: { fileSize: 5 * 1024 * 1024 },
// Only allow certain file types
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
},
// Store with unique names and outside web root
storage: multer.diskStorage({
destination: '/uploads/secure',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
}
})
});
8. Cryptography
Data Encryption
const crypto = require('crypto');
// AES-256-GCM encryption
function encrypt(plaintext, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
data: encrypted,
authTag: authTag.toString('hex')
};
}
function decrypt(encrypted, key, iv, authTag) {
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Secure Random Generation
// GOOD - Use crypto for random values
const crypto = require('crypto');
// Generate a secure random token
function generateToken(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
// Generate a secure password reset token
function generateResetToken() {
return crypto.randomBytes(32).toString('base64url');
}
9. Error Handling
// Don't expose internal details in production
app.use((err, req, res, next) => {
if (process.env.NODE_ENV === 'production') {
console.error(err); // Log internally
// Generic error to user
return res.status(500).json({
error: 'Internal Server Error',
message: 'An unexpected error occurred'
});
}
// Detailed errors in development
res.status(500).json({
error: err.name,
message: err.message,
stack: err.stack
});
});
10. Logging and Monitoring
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Log security-relevant events
app.use((req, res, next) => {
logger.info({
type: 'request',
method: req.method,
path: req.path,
ip: req.ip,
userId: req.user?.id
});
next();
});
// Log authentication events
app.post('/login', async (req, res) => {
const success = await attemptLogin(req.body);
logger.info({
type: 'auth',
action: 'login',
success,
email: req.body.email,
ip: req.ip
});
});
Secure Development Checklist
┌─────────────────────────────────────────────────────────────┐
│ PRE-COMMIT SECURITY CHECKLIST │
├─────────────────────────────────────────────────────────────┤
│ INPUT VALIDATION │
│ [ ] All user inputs validated │
│ [ ] Parameterized queries used │
│ [ ] No string concatenation in queries │
├─────────────────────────────────────────────────────────────┤
│ AUTHENTICATION │
│ [ ] Passwords hashed with bcrypt (12+ rounds) │
│ [ ] Sessions use secure cookies │
│ [ ] MFA implemented for sensitive operations │
├─────────────────────────────────────────────────────────────┤
│ AUTHORIZATION │
│ [ ] RBAC implemented │
│ [ ] Object-level access checks verified │
│ [ ] Least privilege principle followed │
├─────────────────────────────────────────────────────────────┤
│ OUTPUT │
│ [ ] Output encoded for context │
│ [ ] CSP headers configured │
│ [ ] No sensitive data in URLs │
├─────────────────────────────────────────────────────────────┤
│ FILES │
│ [ ] File types validated │
│ [ ] File sizes limited │
│ [ ] Uploaded files stored securely │
├─────────────────────────────────────────────────────────────┤
│ ERRORS │
│ [ ] No stack traces in production │
│ [ ] Errors logged │
│ [ ] Generic error messages to users │
└─────────────────────────────────────────────────────────────┘
Tools for Secure Development
| Category | Tools | |----------|-------| | SAST | SonarQube, CodeQL, ESLint Security | | DAST | OWASP ZAP, Burp Suite | | Dependencies | npm audit, Snyk, Dependabot | | Secrets | GitLeaks, TruffleHog | | Container | Clair, Trivy |
Conclusion
Security is not an afterthought—it's a mindset that should be integrated into every line of code you write. By following these secure coding practices, you significantly reduce the attack surface of your applications.
Key takeaways:
- Validate everything - Never trust user input
- Use parameterized queries - Prevent SQL injection
- Hash passwords properly - Use bcrypt with high rounds
- Implement proper authorization - Check permissions on every access
- Encode output - Prevent XSS attacks
- Log security events - Know what's happening in your system
Need help securing your code?
Need help securing your systems?
Our expert security team can help you identify and fix vulnerabilities before attackers exploit them.
DevSecure Team
Security expert at DevSecure. Passionate about cybersecurity and helping organizations protect their digital assets.