Security checklist
Minimum viable security before shipping any web application. This is not exhaustive—it's the baseline that prevents obvious disasters.
Pre-ship audit
Run through this checklist before any production deployment. If you can't check an item, fix it first.
Authentication
- [ ] Passwords hashed with bcrypt, scrypt, or Argon2 (NEVER MD5/SHA1/plain text)
- [ ] Password requirements enforced (minimum 12 characters recommended)
- [ ] Session tokens are cryptographically random (use
crypto.randomBytesor equivalent) - [ ] Sessions expire (24 hours for normal apps, shorter for sensitive data)
- [ ] Logout actually invalidates the session server-side
- [ ] Password reset tokens are single-use and expire within 1 hour
- [ ] Failed login attempts are rate-limited (5 attempts, then cooldown)
- [ ] No credentials in code, logs, or error messages
Input validation
- [ ] All user input is validated server-side (client validation is UX, not security)
- [ ] SQL queries use parameterized statements (NEVER string concatenation)
- [ ] HTML output is escaped to prevent XSS (use framework defaults)
- [ ] File uploads validate type, size, and are stored outside webroot
- [ ] URLs and redirects are validated against allowlist
- [ ] JSON/XML parsers have entity expansion limits
Secrets management
- [ ] No secrets in source code or git history
- [ ] Environment variables or secrets manager for all credentials
- [ ] Different secrets for development/staging/production
- [ ] API keys have minimum required permissions
- [ ] Secrets can be rotated without code deployment
Database security
- [ ] Database not exposed to public internet
- [ ] Application uses dedicated database user (not root/admin)
- [ ] Database user has minimum required permissions
- [ ] Row-level security (RLS) enabled where applicable
- [ ] Backups encrypted and tested for restoration
- [ ] Connection strings use SSL/TLS
Network and transport
- [ ] HTTPS only (redirect HTTP to HTTPS)
- [ ] TLS 1.2+ (disable older versions)
- [ ] HSTS header enabled
- [ ] Secure cookie flags set (Secure, HttpOnly, SameSite)
- [ ] CORS configured for specific origins (not
*in production)
Rate limiting and DDoS
- [ ] API endpoints rate-limited per user/IP
- [ ] Login endpoints have stricter limits
- [ ] CDN or DDoS protection in front of application
- [ ] Request size limits configured
- [ ] Timeout limits on all external calls
Logging and monitoring
- [ ] Authentication events logged (login, logout, failed attempts)
- [ ] No sensitive data in logs (passwords, tokens, PII)
- [ ] Logs stored securely with retention policy
- [ ] Alerts configured for suspicious patterns
- [ ] Error messages don't leak system information
Infrastructure
- [ ] Firewall configured (deny by default)
- [ ] Unnecessary ports closed
- [ ] SSH key authentication (no password auth)
- [ ] Dependencies updated (check for known vulnerabilities)
- [ ] Admin interfaces not publicly accessible
Common vulnerabilities by framework
Node.js/Express
// BAD: SQL injection
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// GOOD: Parameterized query
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
// BAD: XSS vulnerability
res.send(`<h1>Hello ${req.query.name}</h1>`);
// GOOD: Use template engine with auto-escaping
res.render('greeting', { name: req.query.name });
// BAD: Secrets in code
const API_KEY = 'sk_live_abc123';
// GOOD: Environment variables
const API_KEY = process.env.API_KEY;
Python/Django/Flask
# BAD: SQL injection
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# GOOD: Parameterized query
cursor.execute("SELECT * FROM users WHERE id = %s", [user_id])
# BAD: Pickle deserialization (RCE vulnerability)
data = pickle.loads(user_input)
# GOOD: Use JSON for untrusted data
data = json.loads(user_input)
# BAD: Debug mode in production
app.run(debug=True)
# GOOD: Debug off, proper error handling
app.run(debug=False)
React/Frontend
// BAD: XSS via dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// GOOD: Let React escape content
<div>{userContent}</div>
// BAD: Storing tokens in localStorage (XSS accessible)
localStorage.setItem('token', authToken);
// BETTER: HttpOnly cookies (not accessible via JS)
// Set via server response header
// BAD: Exposing API keys in frontend code
const API_KEY = 'sk_live_abc123';
// GOOD: Proxy through your backend
const response = await fetch('/api/proxy/external-service');
Password hashing reference
Node.js with bcrypt
const bcrypt = require('bcrypt');
// Hashing (on registration)
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
// Verification (on login)
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
Python with bcrypt
import bcrypt
# Hashing
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
# Verification
is_valid = bcrypt.checkpw(password.encode('utf-8'), hashed)
Environment variables setup
.env file (development only, never commit)
# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
JWT_SECRET=your-256-bit-secret-here
API_KEY=sk_test_xxxxx
.gitignore (mandatory)
.env
.env.local
.env.*.local
*.pem
*.key
credentials.json
secrets/
Loading environment variables
// Node.js with dotenv
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
// Fail fast if missing
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required');
}
Database row-level security (Supabase/PostgreSQL)
-- Enable RLS on table
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Users can only read their own documents
CREATE POLICY "Users can read own documents" ON documents
FOR SELECT USING (auth.uid() = user_id);
-- Users can only insert documents as themselves
CREATE POLICY "Users can insert own documents" ON documents
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Users can only update their own documents
CREATE POLICY "Users can update own documents" ON documents
FOR UPDATE USING (auth.uid() = user_id);
-- Users can only delete their own documents
CREATE POLICY "Users can delete own documents" ON documents
FOR DELETE USING (auth.uid() = user_id);
CORS configuration
Express.js
const cors = require('cors');
// BAD: Allow all origins
app.use(cors());
// GOOD: Specific origins
app.use(cors({
origin: ['https://myapp.com', 'https://www.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
Security headers
Express.js with Helmet
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Manual headers
// Essential security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Content-Security-Policy', "default-src 'self'");
What NOT to log
// NEVER log these:
// - Passwords (plain or hashed)
// - Session tokens / JWTs
// - API keys
// - Credit card numbers
// - Social security numbers
// - Full request bodies with user data
// BAD
console.log('Login attempt:', { email, password });
console.log('Request:', req.body);
// GOOD
console.log('Login attempt:', { email, success: false, reason: 'invalid_password' });
console.log('Request:', { endpoint: req.path, method: req.method, userId: req.user?.id });
Quick fixes for common issues
"I stored passwords in plain text"
- Add password hashing immediately
- Force password reset for all users
- Invalidate all existing sessions
- Check if database was ever exposed/leaked
"My API key is in the git history"
- Rotate the key immediately (generate new one)
- Revoke the old key
- Use
git filter-branchor BFG Repo-Cleaner to remove from history - Force push (coordinate with team)
"I don't know what data I'm logging"
- Search codebase for
console.log,logger.,print( - Review what's being logged
- Implement structured logging with field allowlists
- Set up log rotation and retention
"My database is publicly accessible"
- Change database credentials immediately
- Configure firewall rules (allow only application server IPs)
- Enable SSL/TLS for connections
- Review access logs for unauthorized queries
Compliance quick reference
You likely need to care about:
- GDPR (EU users): Data deletion rights, consent, breach notification
- CCPA (California users): Similar to GDPR
- PCI DSS (credit cards): Don't store card numbers, use payment processor
- HIPAA (health data): Encryption, access controls, audit logs
- SOC 2 (enterprise sales): Security controls documentation
Minimum for any user data:
- Privacy policy explaining data collection
- Way for users to request data deletion
- Encryption in transit (HTTPS) and at rest
- Access logs and audit trail
- Incident response plan (even if basic)
Resources
- OWASP Top 10: https://owasp.org/www-project-top-ten/
- OWASP Cheat Sheets: https://cheatsheetseries.owasp.org/
- Have I Been Pwned (check for breaches): https://haveibeenpwned.com/
- Mozilla Observatory (test your headers): https://observatory.mozilla.org/
- SSL Labs (test your TLS): https://www.ssllabs.com/ssltest/