DEV Community

Alex Chen
Alex Chen

Posted on

Web Security Basics Every Developer Must Know (2026)

Web Security Basics Every Developer Must Know (2026)

Security isn't a feature you add later — it's built into every layer. Here's what every developer needs to know to keep their applications safe.

The Threat Model

Who are you protecting against?
1. Script kiddies → Automated scanners, basic attacks
2. Opportunists → Looking for easy targets (unpatched vulnerabilities)
3. Targeted attackers → Specific goals (data theft, ransom)
4. Insiders → Employees with access

What are you protecting?
- User data (PII, passwords, payment info)
- Business data (revenue figures, trade secrets)
- System integrity (availability, reputation)
- User trust (once lost, almost impossible to regain)

The security mindset:
"Never trust input. Always validate. Always sanitize.
Encrypt in transit AND at rest. Use the principle of least privilege."
Enter fullscreen mode Exit fullscreen mode

OWASP Top 10 (2025 Edition) — Practical Guide

// === 1. Broken Access Control ===
// Problem: Users can access resources they shouldn't
// Fix: ALWAYS check authorization on EVERY endpoint

// ❌ Only checks authentication (is user logged in?)
app.get('/api/users/:id', authRequired, async (req, res) => {
  const user = await db.users.findById(req.params.id);
  // Any logged-in user can access ANY user's data!
});

// ✅ Check ownership/authorization
app.get('/api/users/:id', authRequired, async (req, res) => {
  if (req.params.id !== req.user.id && !req.user.isAdmin) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  const user = await db.users.findById(req.params.id);
});

// Resource-based access control pattern:
function requireOwnership(model) {
  return async (req, res, next) => {
    const resource = await model.findById(req.params.id);
    if (!resource) return res.status(404).json({ error: 'Not found' });
    if (resource.userId !== req.user.id && !req.user.isAdmin) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    req.resource = resource;
    next();
  };
}
app.put('/api/posts/:id', authRequired, requireOwnership(Post), updatePost);

// === 2. Cryptographic Failures ===
// Problem: Sensitive data stored or transmitted unencrypted
// Fix: Encrypt at rest, use TLS in transit, proper key management

const crypto = require('crypto');

// Password hashing (NEVER store plaintext!):
async function hashPassword(password) {
  // Argon2id is the current gold standard
  // bcrypt is also excellent and widely supported
  const salt = crypto.randomBytes(16);
  // Use argon2 or bcrypt library:
  // return await argon2.hash(password); 
  return await bcrypt.hash(password, 12); // 12 rounds = good balance of speed/security
}

// Verify password:
async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

// NEVER roll your own crypto!
// ❌ Custom "encryption" = base64 + XOR = NOT encryption
// ✅ Use established libraries: bcrypt, argon2, sodium, node:crypto

// API keys & secrets:
// - Store in environment variables (never in code!)
// - Rotate regularly
// - Use different keys per environment
// - Never log secrets (use redaction middleware)

// Data classification:
// PUBLIC: Can be freely shared (product names, public content)
// INTERNAL: Company internal (employee directory, internal docs)
// CONFIDENTIAL: User PII, business data (encrypt at rest!)
// RESTRICTED: Payment data, health records (PCI/HIPAA compliance)
Enter fullscreen mode Exit fullscreen mode
// === 3. Injection (SQL, NoSQL, Command, LDAP) ===
// Problem: Attacker-supplied data interpreted as commands
// Fix: Parameterized queries, input validation, output encoding

// SQL Injection — the classic:
// ❌ String concatenation (VULNERABLE):
const query = `SELECT * FROM users WHERE id = ${userId}`;
// userId = "1 OR 1=1" → Returns ALL users!

// ✅ Parameterized queries (SAFE):
const user = await db.query(
  'SELECT * FROM users WHERE id = $1',
  [userId]  // Parameterized — database treats as data, not code
);

// NoSQL Injection (MongoDB):
// ❌ Vulnerable:
const user = await db.users.find({ username: req.body.username, password: req.body.password });
// { username: { "$ne": "" }, password: { "$ne": "" } } → Bypasses login!

// ✅ Safe: Type checking + allowlist:
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') {
  return res.status(400).json({ error: 'Invalid input' });
}
const user = await db.users.findOne({
  username: req.body.username,
  password: hashedPassword, // Compare hashes, never raw passwords
});

// Command Injection:
// ❌ VULNER:
const { execSync } = require('child_process');
execSync(`convert ${userInput} output.png`);
// userInput = "; rm -rf / #" → DELETES EVERYTHING!

// ✅ Safe: Validation + allowlist of characters:
function sanitizeFilename(input) {
  if (!/^[a-zA-Z0-9._-]+$/.test(input)) throw new Error('Invalid filename');
  if (input.includes('..')) throw new Error('Path traversal detected');
  return path.basename(input); // Strip any path components
}
execSync(`convert "${sanitizeFilename(userInput)}" output.png`);

// General injection prevention rules:
// 1. Use parameterized queries for ALL database operations
// 2. Validate input type, length, format BEFORE using it
// 3. Use an ORM that handles escaping properly (Prisma, Sequelize, TypeORM)
// 4. For shell commands: avoid exec() entirely, use library APIs instead
// 5. When exec() is unavoidable: strict allowlist validation
Enter fullscreen mode Exit fullscreen mode
// === 4. Insecure Design ===
// Problem: Security not considered during design phase
// Fix: Threat modeling, abuse case testing, secure by default

// Example: Password reset flow
// ❌ Insecure design: Reset token sent via email, no expiration, reusable
// ✅ Secure design:
// - Token expires in 15 minutes
// Token is single-use (invalidated after use)
// Token is cryptographically strong (128+ bits of entropy)
// Rate limiting on reset requests (max 3/hour per email)
// Notify user when password is changed (detect unauthorized resets)
// Log all reset attempts for audit trail

// Abuse cases to think about for EVERY feature:
// "What if someone calls this API 10,000 times?" → Rate limiting
// "What if someone submits <script>alert(1)</script>?" → Input sanitization
// "What if someone modifies the form data?" → Server-side validation
// "What if someone accesses another user's URL?" → Authorization check
// "What if someone intercepts the request?" → CSRF tokens, TLS
Enter fullscreen mode Exit fullscreen mode
// === 5. Security Misconfiguration ===
// Problem: Default configs, unnecessary features, verbose errors
// Fix: Hardened defaults, minimal attack surface, proper headers

// Express.js security middleware setup:
const helmet = require('helmet');           // Security headers
const rateLimit = require('express-rate-limit'); // Rate limiting
const cors = require('cors');               // CORS configuration

// Security headers (Helmet handles most):
app.use(helmet({
  contentSecurityPolicy: {                // Prevents XSS via injected scripts
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"], // Remove unsafe-inline when possible
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
    },
  },
  hsts: { maxAge: 31536000, includeSubDomains: true }, // Force HTTPS
  noSniff: true,                                       // Prevent MIME sniffing
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

// Rate limiting:
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                 // Limit each IP to 100 requests per windowMs
  message: { error: 'Too many requests, please try again later.' },
  standardHeaders: true,
  legacyHeaders: false,
});
app.use(limiter);

// Stricter limits for auth endpoints:
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,                   // Only 5 login attempts per 15 minutes
  message: { error: 'Too many attempts. Account locked for 15 min.' },
});
app.post('/api/login', authLimiter, handleLogin);

// Disable sensitive endpoints in production:
if (process.env.NODE_ENV === 'production') {
  app.disable('x-powered-by');        // Don't reveal Express
  // Don't expose stack traces to users:
  app.use((err, req, res, next) => {
    res.status(err.status || 500).json({
      error: process.env.NODE_ENV === 'production' ? 'Internal error' : err.message,
      ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
    });
  });
}

// CORS — be specific, don't use wildcard in production:
app.use(cors({
  origin: ['https://example.com', 'https://www.example.com'], // Allowlisted only
  credentials: true,              // Allow cookies/auth headers
  methods: ['GET', 'POST', 'PUT', 'DELETE'], // Restrict methods
  allowedHeaders: ['Content-Type', 'Authorization'],
}));
Enter fullscreen mode Exit fullscreen mode
// === 6. Vulnerable & Outdated Components ===
// Problem: Using libraries with known vulnerabilities
// Fix: Regular dependency auditing, automated updates

// Audit dependencies:
npm audit                    # Check for known vulnerabilities
npm audit fix               # Auto-fix where possible
npm outdated                # Check for outdated packages

// Add to CI pipeline:
// npm audit --audit-level=moderate || exit 1

// Lockfile integrity:
// Always commit package-lock.json!
# It ensures everyone installs exact same versions
# Without it: npm install might pull different (vulnerable!) versions

// Automated dependency updates:
# Dependabot (GitHub), Renovate, Snyk
# Set up PRs for security patches automatically

// Pin critical dependencies:
{
  "dependencies": {
    "lodash": "^4.17.21",   // Allow patch/minor updates
    "express": "4.18.2",     // Pin exact version for critical apps
  }
}
Enter fullscreen mode Exit fullscreen mode
// === 7. Authentication & Session Management Failures ===
// Problem: Weak auth, session hijacking, credential stuffing
// Fix: MFA, secure sessions, strong password policies

// Session security:
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,       // Strong random secret (rotate regularly!)
  name: '__Host-sid',                       // Prefix: __Host = cookie only sent over HTTPS
  cookie: {
    secure: true,                           // Only over HTTPS
    httpOnly: true,                         // Not accessible via JavaScript (prevents XSS theft)
    sameSite: 'lax',                        // CSRF protection
    maxAge: 3600000,                        // 1 hour session
    domain: 'example.com',                  // Restrict domain
    path: '/',                              // Cookie path
  },
  rolling: false,                           // Don't extend session on every request
  resave: false,
  saveUninitialized: false,                 // Don't create sessions for unauthenticated users
}));

// JWT best practices:
const jwt = require('jsonwebtoken');

function generateToken(user) {
  return jwt.sign(
    { sub: user.id, role: user.role },     // Minimal payload (no PII!)
    process.env.JWT_SECRET,
    { expiresIn: '15m', algorithm: 'HS256', issuer: 'example.com' }
  );
}

// Short-lived access token + refresh token pattern:
// Access token: 15min (stored in memory/httpOnly cookie)
// Refresh token: 7 days (stored in httpOnly cookie, one-time use, rotated)
// This limits damage from token theft!

// MFA (Multi-Factor Authentication):
// Require MFA for:
// - New device login
// - Password change
// - Sensitive operations (payments, data export)
// - Admin panel access
Enter fullscreen mode Exit fullscreen mode

Security Checklist for Every Deploy

# Before deploying ANYTHING:

[ ] Dependencies audited? (npm audit / pip audit)
[ ] Environment variables set correctly? (No dev secrets in prod!)
[ ] HTTPS/TLS configured?
[ ] Security headers present? (CSP, HSTS, X-Frame-Options)
[ ] Rate limiting enabled?
[ ] CORS configured properly (not wildcard)?
[ ] Error messages sanitized (no stack traces)?
[ ] Logging enabled (but no secrets in logs)?
[ ] Database migrations run with correct permissions?
[ ] Backup strategy tested?
[ ] DDoS protection active? (Cloudflare, AWS Shield)
[ ] Authentication flows tested?
[ ] Input validation on ALL endpoints?
[ ] CSP (Content-Security-Policy) headers set?

# Run this checklist automatically in CI:
# npx snyk test          # Dependency vulnerability scan
# npm run security:test  # Your custom security tests
Enter fullscreen mode Exit fullscreen mode

Security is a journey, not a destination. What's the most important security lesson you've learned the hard way?

Follow @armorbreak for more practical developer guides.

Top comments (0)