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
javascript
// 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
python
# 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
javascript
// 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
javascript
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
python
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)
bash
# .env DATABASE_URL=postgresql://user:pass@localhost:5432/myapp JWT_SECRET=your-256-bit-secret-here API_KEY=sk_test_xxxxx
.gitignore (mandatory)
gitignore
.env .env.local .env.*.local *.pem *.key credentials.json secrets/
Loading environment variables
javascript
// 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)
sql
-- 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
javascript
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
javascript
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
javascript
// 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
javascript
// 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/