The articles outline common web application vulnerabilities aligned with the OWASP Top 10 and provide example fixes in typical developer workflows. They emphasize that security is built into authentication and authorization, server-side validation, and safe handling of data and errors. For broken access control, sources show that APIs must verify resource ownership (and allow elevated roles where appropriate) instead of returning data based only on an ID in the request. For cryptographic failures, they stress using strong password hashing (bcrypt with an appropriate work factor) and avoiding fast hashes like MD5/SHA1 for password storage, while also using authenticated encryption such as AES-GCM for other sensitive data and managing keys via environment variables or a secrets manager. For injection risks, they recommend parameterized SQL queries, input sanitization and strict query structure for NoSQL (e.g., MongoDB), and allowlist/validation to prevent command injection. For insecure design and misconfiguration, they call for cryptographically secure tokens, server-side rate limiting and lockout mechanisms, generic error responses in production, security headers via Helmet/CSP, and disabling debug endpoints and other risky defaults. They also highlight dependency scanning (npm audit in CI), stronger authentication token lifetimes and refresh token handling, and ensuring data integrity with server-side recalculation and database transactions where concurrent updates matter.
OWASP Top 10 and core web security practices highlighted for developers
The articles outline common web application vulnerabilities aligned with the OWASP Top 10 and provide example fixes in typical developer workflows. They emphasize that security is built into authentic...
- Broken access control requires verifying that the logged-in user owns the requested resource (or is authorized as an admin).
- Passwords must be hashed with a strong, slow algorithm such as bcrypt; avoid MD5/SHA1 for password storage.
- Injection prevention relies on parameterized queries for SQL, sanitized/strict inputs for NoSQL, and strict allowlist/validation to avoid command injection.
- Security should use server-side controls such as cryptographically secure password reset tokens, rate limiting, and account lockout after repeated failed logins.
- Production deployment should avoid detailed error/stack traces and risky defaults by using generic error messages and security headers (e.g., Helmet/CSP).
Web Security Basics: Every Developer Must Know (2026) Security isn't just for security teams. Every developer who writes code that touches the internet needs these fundamentals. The Threat Model Who attacks your app? 1. Script kiddies — automated scanners looking for easy targets 2. Opportunists — exploiting known vulnerabilities in popular frameworks 3. Targeted attackers — after YOUR specific data or users 4. Insiders — employees or contractors with access What do they want? - User data (passwords, PII, payment info) - Compute resources (crypto mining, botnets) - Access to other systems (lateral movement) - Reputation damage - Ransom The 80/20 rule: 20% of security practices prevent 80% of common attacks. This guide covers that 20%. Input Validation: Never Trust the Client // Rule #1: All user input is hostile until proven otherwise // Rule #2: Validate on the SERVER (client validation is UX only) // Rule #3: Use allowlists over blocklists when possible // ❌ Bad: Blocklist approach (easy to bypass) function sanitize(input) { return input.replace(/<script>/gi, ''); // What about <SCRIPT> or <img onerror=...>? } // ✅ Good: Structured validation with schema library const Joi = require('joi'); const schemas = { // Registration input register: Joi.object({ email: Joi.string().email().lowercase().max(254).required(), password: Joi.string() .min(12) .max(128) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])/) .message('Password must be 12+ chars with uppercase, lowercase, digit, and special char'), username: Joi.string().alphanum().min(3).max(30).required(), inviteCode: Joi.string().alphanum().length(8).optional(), termsAccepted: Joi.boolean().valid(true).required(), }), // Query parameters for list endpoint listQuery: Joi.object({ page: Joi.number().integer().min(1).default(1), limit: Joi.number().integer().min(1).max(100).default(20), sort: Joi.string().valid('createdAt', 'name', 'views').default('createdAt'), order: Joi.string().valid('asc', 'desc').default('desc'), search: Joi.string().max(200).allow('', null), // Sanitize before DB use! category: Joi.string().alphanum().optional(), }), // ID parameter resourceId: Joi.object({ id: Joi.string().uuid().required(), // UUID format prevents injection }), }; // Middleware to use these schemas: function validate(schema) { return (req, res, next) => { const { error, value } = schemas[schema].validate( { ...req.body, ...req.params, ...req.query }, { stripUnknown: true, abortEarly: false } ); if (error) { const details = error.details.map(d => ({ field: d.path.join('.'), message: d.message, })); return res.status(422).json({ error: 'Validation failed', details }); } // Replace request data with validated/sanitized version Object.assign(req, value); next(); }; } Authentication & Session Security // Password handling: const bcrypt = require('bcrypt'); const SALT_ROUNDS = 12; // Increase as hardware gets faster async function hashPassword(password) { return bcrypt.hash(password, SALT_ROUNDS); // Auto-salt included } async function verifyPassword(password, hash) { return bcrypt.compare(password, hash); // Timing-safe comparison } // JWT best practices: const jwt = require('jsonwebtoken'); const tokenConfig = { accessToken: { expiresIn: '15m', // Short-lived! Refresh via refresh token algorithm: 'HS256', }, refreshToken: { expiresIn: '7d', // Longer-lived, stored securely algorithm: 'HS256', }, }; function generateTokens(user) { const payload = { sub: user.id, email: user.email, role: user.role }; const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: tokenConfig.accessToken.expiresIn, issuer: 'myapp.com', audience: 'myapp.com/api', }); const refreshToken = jwt.sign( { sub: user.id, type: 'refresh' }, process.env.JWT_REFRESH_SECRET, { expiresIn: tokenConfig.refreshToken.expiresIn } ); return { accessToken, refreshToken }; } // Token validation middleware: function authenticate(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'Missing authorization header' }); } const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, process.env.JWT_SECRET, { issuer: 'myapp.com', audience: 'myapp.com/api', algorithms: ['HS256'], // Explicitly specify allowed algorithms! }); req.user = decoded; next(); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' }); } return res.status(401).json({ error: 'Invalid token', code: 'INVALID_TOKEN' }); } } // Session security (if using cookie-based sessions): const sessionConfig = { name: 'sessionId', // Don't use default "connect.sid" secret: crypto.randomBytes(32).toString('hex'), // Strong random secret cookie: { secure: true, // Only send over HTTPS (MUST have this!) httpOnly: true, // JavaScript can't read it (prevents XSS token theft) sameSite: 'strict', // Prevents CSRF maxAge: 3600000, // 1 hour path: '/', // Only needed at root }, rolling: true, // Reset expiry on each activity resave: false, saveUninitialized: false, // Don't create sessions for unauthenticated users }; SQL Injection Prevention // The only safe way: Parameterized queries (always!) // ❌ NEVER string concatenation: const query = `SELECT * FROM users WHERE id = ${userId}`; // userId = "1 OR 1=1" → returns ALL rows! // ✅ ALWAYS parameterized: const result = await db.query( 'SELECT * FROM users WHERE id = $1 AND role = $2', [userId, role] ); // For dynamic queries (when table/column names are variable): const allowedSortFields = ['name', 'email', 'created_at']; const sortField = allowedSortFields.includes(req.query.sort) ? req.query.sort : 'created_at'; const sortOrder = req.query.order === 'asc' ? 'ASC' : 'DESC'; await db.query(`SELECT * FROM users ORDER BY ${sortField} ${sortOrder} LIMIT $1 OFFSET $2`, [limit, offset]); // ORM safety (most ORMs use parameterized queries by default): // Sequelize: User.findAll({ where: { id: userId } }); // Safe! // But be careful with raw queries: sequelize.query(`SELECT * FROM users WHERE name = '${name}'`); // UNSAFE! sequelize.query('SELECT * FROM users WHERE name = ?', { replacements: [name] }); // SAFE XSS Prevention // Output encoding is the primary defense: // In Express/EJS templates: // Use <%= %> for HTML-escaped output (default and SAFE!) // Use <%- %> ONLY for pre-sanitized/trusted content (rarely needed) // React/Vue/Angular: Auto-escaped by default! // dangerouslySetInnerHTML is DANGEROUS — avoid or sanitize first: import DOMPurify from 'dompurify'; function renderMarkdown(content) { const html = marked.parse(content); // Convert markdown to HTML const clean = DOMPurify.sanitize(html, { // Remove dangerous tags/attributes ALLOWED_TAGS: ['p', 'strong', 'em', 'ul', 'ol', 'li', 'code', 'pre', 'a', 'h1', 'h2', 'h3'], ALLOWED_ATTR: ['href', 'class'], }); return clean; } // Content Security Policy (CSP): Defense in depth // In helmet() configuration: app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], // Remove unsafe-inline once you can styleSrc: ["'self'", "'unsafe-inline'", 'fonts.googleapis.com'], fontSrc: ["'self'", 'fonts.gstatic.com'], imgSrc: ["'self'", 'data:', 'https:'], // Allow HTTPS images connectSrc: ["'self'", 'https://api.example.com'], frameAncestors: ["'none'"], // Prevent clickjacking formAction: ["'self'"], baseUri: ["'self'"], objectEmbed: ["'none'"], upgradeInsecureRequests: [], // Force HTTPS everywhere }, })); CORS Configuration // Don't use wildcard (*) in production! const corsOptions = { origin: function (origin, callback) { const allowedOrigins = [ 'https://myapp.com', 'https://www.myapp.com', 'https://staging.myapp.com', ]; // Allow requests from no origin (mobile apps, Postman, etc.) if (!origin) return callback(null, true); if (allowedOrigins.indexOf(origin) !== -1 || origin.endsWith('.myapp.com')) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, // Allow cookies/auth headers methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], maxAge: 86400, // Preflight cache for 24 hours optionsSuccessStatus: 204 // No content for preflight }; app.use(cors(corsOptions)); Rate Limiting: Protect Against Abuse const rateLimit = require('express-rate-limit'); // Different limits for different endpoints: const limits = { // General API: 100 req/min per IP general: rateLimit({ windowMs: 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, message: { error: 'Too many requests, please try again later' }, }), // Auth endpoints: stricter (5 req/min per IP) auth: rateLimit({ windowMs: 60 * 1000, max: 5, skipSuccessfulRequests: false, // Count successful login attempts too! keyGenerator: (req) => req.ip, handler: (req, res) => { res.status(429).json({ error: 'Too many attempts. Please wait before trying again.', retryAfter: Math.ceil(req.rateLimit.resetTime / 1000), }); }, }), // Uploads: very strict (10 req/hour per IP) upload: rateLimit({ windowMs: 60 * 60 * 1000, max: 10, }), }; app.use('/api/', limits.general); app.use('/api/auth/', limits.auth); app.use('/api/upload/', limits.upload); What security practice took you too long to learn? What would you add to this checklist? Follow @armorbreak for more practical developer guides.
10 hours agoWeb Security: OWASP Top 10 for Developers (2026) Security isn't a feature you add later — it's built into how you code. Here's what every developer needs to know about the OWASP Top 10. What is OWASP Top 10? OWASP = Open Web Application Security Project Top 10 = The 10 most critical web application security risks This is the industry standard. If you're building web apps, you need to know these. Not knowing is not an excuse. #1 Broken Access Control // ❌ Vulnerable: Users can access other users' data app.get('/api/users/:id', async (req, res) => { const user = await db.users.findById(req.params.id); // Anyone can request any ID! No authorization check! res.json(user); }); // ✅ Secure: Check ownership app.get('/api/users/:id', authMiddleware, async (req, res) => { // Only allow users to access their OWN data if (req.params.id !== req.user.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Forbidden' }); } const user = await db.users.findById(req.params.id); res.json(user); }); // ✅ Better: Use middleware for all protected routes function requireOwnership(resourceType) { return async (req, res, next) => { const resource = await db[resourceType].findById(req.params.id); if (!resource) return res.status(404).json({ error: 'Not found' }); // Resource belongs to current user OR user is admin if (resource.userId !== req.user.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Access denied' }); } req.resource = resource; next(); }; } // Usage: app.get('/api/posts/:id', auth, requireOwnership('posts'), getPost); #2 Cryptographic Failures (was Sensitive Data Exposure) // ❌ Storing passwords in plain text or weak hash const password = 'user_password'; db.users.insert({ email, password }); // PLAIN TEXT! Criminal negligence // ❌ Using MD5 or SHA1 (fast to crack with rainbow tables) const hashed = crypto.createHash('md5').update(password).digest('hex'); // ✅ Secure: bcrypt with proper work factor const bcrypt = require('bcrypt'); const saltRounds = 12; // Higher = slower = more secure (adjust per hardware) const hashedPassword = await bcrypt.hash(password, saltRounds); // Verification: const match = await bcrypt.compare(inputPassword, storedHash); // ✅ For passwords, ALWAYS use: // - bcrypt, scrypt, or Argon2id (NOT SHA/MD5) // - Salt (unique per password) // - High work factor (bcrypt cost ≥ 12) // - NEVER roll your own crypto! // ✅ Sensitive data in transit — HTTPS everywhere // In Express: const helmet = require('helmet'); app.use(helmet()); // Sets security headers including HSTS // Force HTTPS redirect: if (process.env.NODE_ENV === 'production') { app.use((req, res, next) => { if (req.header('x-forwarded-proto') !== 'https') { return res.redirect(`https://${req.header('host')}${req.url}`); } next(); }); } #3 Injection (SQL, NoSQL, Command, XSS) -- SQL Injection: The classic vulnerability -- ❌ Vulnerable (string concatenation): SELECT * FROM users WHERE email = '$email' AND password = '$password' -- Attacker inputs: email = "' OR 1=1 --" -- Result: SELECT * FROM users WHERE email = '' OR 1=1 --' AND password = '' -- Returns ALL users! Bypasses authentication entirely! -- ✅ Secure (parameterized queries / prepared statements): SELECT * FROM users WHERE email = ? AND password = ? -- Parameters are sent separately, never interpreted as SQL code // JavaScript implementation: // ❌ SQL Injection vulnerable: const query = `SELECT * FROM products WHERE category = '${req.query.category}'`; db.query(query); // Attacker sets category = '; DROP TABLE products; -- // ✅ Parameterized query (prevents SQL injection): const query = 'SELECT * FROM products WHERE category = ?'; db.query(query, [req.query.category]); // Safe! // Or using query builder (Knex.js): knex('products').where('category', req.query.category).select(); // ❌ NoSQL Injection (MongoDB): const query = { username: req.body.username, password: req.body.password }; // Attacker sends: username: { "$gt": "" }, password: { "$gt": "" } // Matches ANY document! Bypasses auth entirely. // ✅ NoSQL safe (type checking + schema validation): const { object } = require('yup'); const loginSchema = object({ username: string().required().max(100), password: string().required().max(100), }); await loginSchema.validate(req.body); // Rejects non-string values! db.users.findOne({ username: req.body.username, password: req.body.password }); // ❌ Command Injection: const { exec } = require('child_process'); exec(`convert ${req.body.filename} output.png`); // Attacker: filename = "; rm -rf /; echo " // ✅ Never pass user input to shell commands! // Use libraries instead of shell commands: const sharp = require('sharp'); await sharp(req.file.path).png().toFile('output.png'); // ❌ Cross-Site Scripting (XSS): res.send(`<h1>Hello, ${req.query.name}</h1>`); // Attacker: name = "<script>stealCookies()</script>" // ✅ Output encoding / templating engines: res.render('greeting', { name: req.query.name }); // Auto-escaped by template engine // Or manual escaping: const escaped = req.query.name.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]); #4 Insecure Design // ❌ Insecure by design: Password reset uses predictable tokens function generateResetToken() { return Math.random().toString(36).substring(7); // Predictable! Guessable! } // Token: "x7f9a2b" → attacker can brute force this easily // ✅ Secure design: Cryptographically secure random tokens const crypto = require('crypto'); function generateResetToken() { return crypto.randomBytes(32).toString('hex'); // 64 hex chars = 256 bits } // Token: "a7f9c2e8..." → 2^256 possible values → impossible to brute force // ❌ Insecure design: Rate limiting on client side // <script> // let attempts = 0; // function tryLogin() { if (attempts < 5) { attempts++; submitLogin(); } } // </script> // Client can just remove this check! // ✅ Secure design: Server-side rate limiting const rateLimit = require('express-rate-limit'); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window message: { error: 'Too many attempts. Try again later.' }, standardHeaders: true, }); app.post('/api/login', loginLimiter, handleLogin); // ❌ Insecure design: Trusting client input for business logic app.post('/api/purchase', (req, res) => { const { itemId, price } = req.body; // Client sends the price! They can set price = $0.01 for anything! processPayment(price); }); // ✅ Secure design: Server validates everything app.post('/api/purchase', auth, async (req, res) => { const item = await db.items.findById(req.body.itemId); if (!item) return res.status(404).json({ error: 'Item not found' }); const price = item.price; // Use SERVER price, not client price! // Apply server-side discounts only if (req.user.hasDiscount) price *= 0.9; await processPayment(price, req.user.id); }); #5 Security Misconfiguration // ❌ Exposing stack traces in production app.use((err, req, res, next) => { res.status(500).json({ error: err.message, stack: err.stack }); // Leaks file paths, library versions, internal architecture! }); // ✅ Production-safe error handler app.use((err, req, res, next) => { const isDev = process.env.NODE_ENV !== 'production'; res.status(err.statusCode || 500).json({ error: isDev ? err.message : 'Internal server error', ...(isDev && { stack: err.stack }), incidentId: err.incidentId, // For support lookup }); }); // ❌ Default credentials, unnecessary features enabled // Running with DEBUG=true in production // Leaving admin panel at /admin with default password // CORS allowing all origins (*) // ✅ Security headers via Helmet: const helmet = require('helmet'); app.use(helmet({ contentSecurityPolicy: { // Prevents XSS/data injection directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "cdn.example.com"], styleSrc: ["'self'", "'unsafe-inline'"], // If needed for CSS frameworks imgSrc: ["'self'", "data:", "https:"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true }, // Force HTTPS noSniff: true, // Prevent MIME sniffing referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, })); // ✅ CORS configuration (only allow your frontend): const cors = require('cors'); app.use(cors({ origin: ['https://yourdomain.com', 'https://www.yourdomain.com'], methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, })); // Never use cors() without options (allows ALL origins)! #6 Vulnerable & Outdated Components # Check for vulnerabilities DAILY: npm audit # Show known vulnerabilities npm audit fix # Auto-fix where possible (safe fixes only) npm audit fix --force # Fix breaking changes too (review carefully!) # Integrate into CI: # .github/workflows/security.yml name: Security Audit on: [push, pull_request] jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: npm install - run: npm audit --audit-level=moderate # Fail on moderate+ severity # Automated dependency updates: # Use Dependabot (GitHub native) or Renovate # .github/dependabot.yml version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: weekly open-pull-requests-limit: 10 # Auto-merge security patches: groups: production-dependencies: patterns: "*" update-types: - minor - patch #7 Identification & Authentication Failures // ❌ Weak password policy allows "123456" // ✅ Strong password requirements: const passwordSchema = yup.object({ password: string() .min(12, 'Must be at least 12 characters') .matches(/[a-z]/, 'Must contain lowercase letter') .matches(/[A-Z]/, 'Must contain uppercase letter') .matches(/\d/, 'Must contain digit') .matches(/[^a-zA-Z0-9]/, 'Must contain special character'), }); // ❌ No account lockout (infinite brute force attempts) // ✅ Account lockout after failed attempts: const MAX_ATTEMPTS = 5; const LOCKOUT_TIME = 30 * 60 * 1000; // 30 minutes async function handleFailedLogin(userId) { await db.loginAttempts.increment(userId); const attempts = await db.loginAttempts.count(userId, LOCKOUT_TIME); if (attempts >= MAX_ATTEMPTS) { await db.users.lockAccount(userId, new Date(Date.now() + LOCKOUT_TIME)); sendLockoutEmail(userId); } } // ❌ Session tokens that never expire // ✅ Short-lived JWTs + refresh token rotation: const jwt = require('jsonwebtoken'); function generateTokens(user) { const accessToken = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' } // Short-lived! 15 minutes max ); const refreshToken = crypto.randomBytes(64).toString('hex'); // Opaque token return { accessToken, refreshToken }; } // Store refresh tokens server-side, rotate on each use: // When user presents old refresh token → issue NEW one, invalidate old // Detect if same refresh token used twice → possible theft, revoke all! // ✅ Multi-Factor Authentication (MFA): // Implement TOTP (Time-based One-Time Password) using otplib: const { authenticator } = require('otplib'); authenticator.options = { step: 30 }; // 30-second codes function verifyTOTP(secret, token) { return authenticator.verify({ secret, token }); } #8 Software & Data Integrity Failures // ❌ Loading scripts from CDN without integrity check <script src="https://cdn.example.com/jquery.min.js"></script> // If CDN is compromised, users load malicious code! // ✅ Subresource Integrity (SRI) hashes: <script src="https://cdn.example.com/jquery.min.js" integrity="sha384-abc123...hash..." crossorigin="anonymous"></script> // Browser verifies hash before executing. Mismatch = blocked! // ❌ Installing packages without verifying: npm install suspicious-package # Could contain anything! // ✅ Verify before installing: npm view suspicious-package # Check publisher, downloads, last publish npm audit # Check for vulnerabilities # Use npm ci (uses lockfile exactly) instead of npm install in CI # Pin dependencies with package-lock.json or yarn.lock // ✅ CI/CD pipeline protection: # Require signed commits in GitHub: # .github/branch-protection.yml requires: # - Signed commits # - Status checks passing # - Code review approval # Use tools like Sigstore/Cosign for container image signing #9 Security Logging & Monitoring Failures // ❌ No logging of security events // ✅ Comprehensive security logging: const securityLogger = { loginSuccess(req, user) { logger.info('LOGIN_SUCCESS', { userId: user.id, ip: req.ip, userAgent: req.get('user-agent'), timestamp: new Date(), }); }, loginFailure(req, reason) { logger.warn('LOGIN_FAILURE', { ip: req.ip, userAgent: req.get('user-agent'), reason, // 'wrong_password', 'account_locked', etc. timestamp: new Date(), }); }, permissionDenied(req, resource, action) { logger.warn('PERMISSION_DENIED', { userId: req.user?.id, ip: req.ip, resource, action, timestamp: new Date(), }); }, suspiciousActivity(req, type, details) { logger.error('SUSPICIOUS_ACTIVITY', { type, // 'rate_limit_exceeded', 'token_reuse', 'brute_force_detected' details, ip: req.ip, timestamp: new Date(), }); // Also trigger alerting: alertTeam(type, details, req.ip); }, }; // ✅ Set up alerting thresholds: // - More than 5 failed logins from same IP in 15 min → Alert // - Access from unusual country → Alert // - Admin actions outside business hours → Alert // - Multiple accounts from same device → Alert #10 Server-Side Request Forgery (SSRF) // ❌ User can specify any URL for server to fetch app.post('/api/fetch-url', async (req, res) => { const data = await fetch(req.body.url); // User controls URL! res.json(await data.json()); }); // Attacker: url = http://169.254.169.254/latest/meta-data/ (AWS metadata!) // Or: url = http://internal-admin-panel:3000/admin/delete-all // ✅ SSRF protection: Allowlist approach const ALLOWED_DOMAINS = [ 'api.public-service.com', 'cdn.trusted-source.net', ]; function isAllowedUrl(url) { try { const parsed = new URL(url); // Block private/internal IP ranges if (isPrivateIP(parsed.hostname)) return false; // Only allow specific domains return ALLOWED_DOMAINS.some(d => parsed.hostname === d || parsed.hostname.endsWith('.' + d)); } catch { return false; // Invalid URL } } function isPrivateIP(hostname) { // Resolve hostname to IP first (DNS rebinding protection!) // Then check against private ranges: // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8 // ::1, fc00::/7, fe80::/10, 169.254.0.0/16 } app.post('/api/fetch-url', async (req, res) => { if (!isAllowedUrl(req.body.url)) { return res.status(403).json({ error: 'URL not allowed' }); } const data = await fetch(req.body.url, { timeout: 5000, maxRedirects: 3 }); res.json(await data.json()); }); Which OWASP Top 10 vulnerability have you encountered most? How do you handle security in your projects? Follow @armorbreak for more practical developer guides.
6 days agoWeb Security: OWASP Top 10 and How to Fix Them (2026) Security isn't a feature you add later — it's built into every layer. Here's how the top 10 vulnerabilities work and how to prevent them. #1 Broken Access Control // ❌ Vulnerable: User can access anyone's data app.get('/api/users/:id', (req, res) => { const user = await db.users.findById(req.params.id); res.json(user); // No check if requester owns this data! }); // ✅ Secure: Always verify ownership app.get('/api/users/:id', async (req, res) => { // Check: Is the logged-in user requesting their OWN data? if (req.params.id !== req.user.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Access denied' }); } const user = await db.users.findById(req.params.id); res.json(user); }); // ✅ Better: Use middleware for all protected routes const requireOwnership = (resourceType) => async (req, res, next) => { const resource = await db[resourceType].findById(req.params.id); if (!resource) return res.status(404).json({ error: 'Not found' }); if (resource.userId !== req.user.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Access denied' }); } req.resource = resource; // Attach for route handler next(); }; app.get('/api/posts/:id', auth, requireOwnership('posts'), (req, res) => { res.json(req.resource); }); #2 Cryptographic Failures // ❌ Storing passwords in plain text or weak hashing const password = "password123"; db.users.insert({ email, password }); // NEVER DO THIS! // ✅ Proper password hashing with bcrypt const bcrypt = require('bcrypt'); const SALT_ROUNDS = 12; // Higher = slower = more secure (12 is good balance) async function hashPassword(password) { return bcrypt.hash(password, SALT_ROUNDS); } async function comparePassword(password, hash) { return bcrypt.compare(password, hash); // Handles salt automatically } // Usage: const hashedPassword = await hashPassword("user_password"); // $2b$12$N9qo8uLOickG2SODUUS... (60 chars, includes salt + hash) const isValid = await comparePassword(input, hashedPassword); // ❌ Using MD5/SHA1 for passwords (too fast, vulnerable to rainbow tables) const md5Hash = crypto.createHash('md5').update(password).digest('hex'); // Cracked in milliseconds with rainbow tables // ✅ For data encryption (not passwords!): const crypto = require('crypto'); function encrypt(text, key) { const iv = crypto.randomBytes(16); // Unique IV per encryption! const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key), iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') }; } function decrypt(encryptedData, key) { const decipher = crypto.createDecipheriv( 'aes-256-gcm', Buffer.from(key), Buffer.from(encryptedData.iv, 'hex') ); decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex')); let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // ⚠️ Key management: // - Never hardcode keys in source code! // - Use environment variables or secrets manager (AWS KMS, HashiCorp Vault) // - Rotate keys regularly #3 Injection (SQL, NoSQL, Command) // === SQL Injection === // ❌ String concatenation in queries app.get('/search', (req, res) => { db.query(`SELECT * FROM products WHERE name LIKE '%${req.query.q}%'`); // Attacker sends q = "'; DROP TABLE users; --" // Result: SELECT * FROM products WHERE name LIKE ''; DROP TABLE users; --' }); // ✅ Parameterized queries (always!) app.get('/search', async (req, res) => { const results = await db.query( 'SELECT * FROM products WHERE name LIKE ?', [`%${req.query.q}%`] // Safely escaped by driver ); res.json(results); }); // With ORM (even safer): const results = await Product.findAll({ where: { name: { [Op.like]: `%${req.query.q}%` } } }); // === NoSQL Injection === // ❌ Passing user input directly to MongoDB query app.post('/login', async (req, res) => { const user = await db.collection('users').findOne({ username: req.body.username, password: req.body.password }); // Attacker sends: username: {"$gt": ""}, password: {"$gt": ""} // Matches ANY document! // ✅ Use strict equality checks const user = await db.collection('users').findOne({ username: req.body.username, password: req.body.password }, { // Disable operators that could be exploited sanitizeFilter: true // MongoDB option }); // Or use a library like mongo-sanitize: const sanitize = require('mongo-sanitize'); const cleanInput = sanitize(req.body); }); // === Command Injection === // ❌ Running shell commands with user input const { exec } = require('child_process'); app.get('/ping', (req, res) => { exec(`ping -c 4 ${req.body.ip}`, (err, stdout) => { res.send(stdout); }); // Attacker sends ip = "127.0.0.1; rm -rf /" // Executes BOTH commands! }); // ✅ Validate input strictly (allowlist approach) const VALID_IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/; app.get('/ping', (req, res) => { if (!VALID_IP_REGEX.test(req.body.ip)) { return res.status(400).json({ error: 'Invalid IP address' }); } exec(`ping -c 4 ${req.body.ip}`, (err, stdout) => { res.send(stdout); }); }); #4 Insecure Design // ❌ Design flaw: Password reset token is predictable function generateResetToken() { return Math.random().toString(36).substring(7); // Weak randomness! } // ✅ Design fix: Use cryptographically secure random tokens const crypto = require('crypto'); function generateResetToken() { return crypto.randomBytes(32).toString('hex'); // 64 hex chars, unpredictable } // Store with expiry: await db.passwordResets.insert({ userId, token: generateResetToken(), expiresAt: Date.now() + 15 * 60 * 1000, // 15 minutes used: false }); // ❌ Design flaw: Rate limiting on client side only // <button onclick="submitForm()">Submit</button> // <script>let clicks=0; function submitForm(){if(clicks<5){clicks++; ...}}</script> // Attacker just ignores JavaScript and sends unlimited requests. // ✅ Server-side rate limiting: const rateLimit = require('express-rate-limit'); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Max 5 attempts per IP message: { error: 'Too many attempts. Try again in 15 minutes.' }, standardHeaders: true, legacyHeaders: false, }); app.post('/api/login', loginLimiter, handleLogin); // ❌ Design flaw: No account lockout after failed attempts // Brute force can try billions of combinations over time // ✅ Account lockout mechanism: async function handleFailedLogin(userId) { const attempts = await incrementLoginAttempts(userId); if (attempts >= 5) { await lockAccount(userId, 30 * 60 * 1000); // Lock for 30 minutes sendSecurityEmail(userId, 'Account locked due to suspicious activity'); } } #5 Security Misconfiguration // ❌ Exposing stack traces in production app.use((err, req, res, next) => { res.status(500).json({ error: err.message, stack: err.stack }); // Reveals file paths, library versions, internal structure! // ✅ Generic error in production, details in logs const isDev = process.env.NODE_ENV !== 'production'; res.status(err.statusCode || 500).json({ error: isDev ? err.message : 'Internal server error', ...(isDev && { stack: err.stack }), incidentId: generateIncidentId(), // For support lookup }); logger.error(`[${incidentId}]`, err.stack); // Details go to logs only }); // ❌ Missing security headers // Without headers: Clickjacking possible, MIME sniffing, etc. // ✅ Add security headers with Helmet: const helmet = require('helmet'); app.use(helmet()); // Adds: // X-Frame-Options: DENY (prevents clickjacking) // X-Content-Type-Options: nosniff (prevents MIME sniffing) // X-XSS-Protection: mode=block // Referrer-Policy: strict-origin-when-cross-origin // Content-Security-Policy (configurable) // Strict-Transport-Security (HTTPS enforcement) // Custom CSP configuration: app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "cdn.example.com"], styleSrc: ["'self'", "'unsafe-inline'"], // Needed for some CSS frameworks imgSrc: ["'self'", "data:", "https:"], fontSrc: ["'self'"], connectSrc: ["'self'", "api.example.com"], frameAncestors: ["'none'"], // Prevents clickjacking formAction: ["'self'"], upgradeInsecureRequests: [], // Force HTTPS }, })); // ❌ Debug endpoints left enabled in production if (process.env.DEBUG) { app.get('/debug/routes', (req, res) => res.json(app._router.stack)); app.get('/debug/env', (req, res) => res.json(process.env)); } // Forgets to disable DEBUG in production → leaks everything! // ✅ Environment-based feature flags: const debugRoutes = express.Router(); debugRoutes.get('/routes', (req, res) => res.json(app._router.stack)); if (process.env.NODE_ENV === 'development') { app.use('/debug', debugRoutes); } // Production simply doesn't mount these routes at all. #6 Vulnerable & Outdated Components # The problem: Dependencies have known vulnerabilities # Attackers scan for outdated packages with public CVEs # Check your dependencies: npm audit # Shows known vulnerabilities npm audit --fix # Auto-fixes where possible (patch/minor updates) # Automated scanning in CI: # .github/workflows/security.yml name: Security Audit on: [push, pull_request] jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm audit --audit-level=moderate # Fails CI if moderate+ severity vulns found # Lockfile integrity: npm ci # Uses package-lock.json exactly (reproducible installs) # vs npm install # May update versions (unpredictable!) # Regular dependency updates: # Use Dependabot (GitHub native), Renovate, or Snyk # Set up weekly PRs for dependency updates #7 Authentication Failures // ❌ Weak session management app.post('/login', (req, res) => { const token = jwt.sign({ userId: user.id }, SECRET_KEY); res.json({ token }); // Never expires! }); // ✅ Secure JWT implementation const jwt = require('jsonwebtoken'); function generateTokens(user) { const accessToken = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' } // Short-lived access token ); const refreshToken = crypto.randomBytes(40).toString('hex'); // Store refresh token in DB with expiry await db.refreshTokens.insert({ token: refreshToken, userId: user.id, expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days }); return { accessToken, refreshToken }; } // Token refresh endpoint: app.post('/refresh', async (req, res) => { const { refreshToken } = req.body; const stored = await db.refreshTokens.findOne({ token: refreshToken }); if (!stored || stored.expiresAt < Date.now() || stored.revoked) { return res.status(401).json({ error: 'Invalid or expired refresh token' }); } const user = await db.users.findById(stored.userId); const tokens = generateTokens(user); // Revoke old refresh token await db.refreshTokens.update(stored.id, { revoked: true }); res.json(tokens); }); // ❌ Allowing weak passwords if (password.length >= 6) accept(); // Too short! // ✅ Password strength requirements: function validatePasswordStrength(password) { const issues = []; if (password.length < 12) issues.push('At least 12 characters'); if (!/[a-z]/.test(password)) issues.push('One lowercase letter'); if (!/[A-Z]/.test(password)) issues.push('One uppercase letter'); if (!/\d/.test(password)) issues.push('One number'); if (!/[!@#$%^&*]/.test(password)) issues.push('One special character'); // Also check against common/breached passwords if (COMMON_PASSWORDS.includes(password.toLowerCase())) { issues.push('This password is too common'); } return issues.length === 0 ? null : issues; } #8 Data Integrity Failures // ❌ Trusting client-side data blindly app.post('/api/orders', (req, res) => { const { total, items } = req.body; // Client sent total = $0.01 for $1000 worth of items! createOrder(total, items); }); // ✅ Server-side validation and recalculation app.post('/api/orders', auth, async (req, res) => { const { items } = req.body; // Validate each item exists and price is current const validatedItems = []; for (const item of items) { const product = await Product.findById(item.productId); if (!product || !product.active) { return res.status(400).json({ error: `Invalid product: ${item.productId}` }); } // Recalculate price from DB (never trust client's price!) validatedItems.push({ productId: item.productId, quantity: Math.min(item.quantity, product.maxOrderQuantity), unitPrice: product.price, // From database, not request! subtotal: product.price * Math.min(item.quantity, product.maxOrderQuantity) }); } const total = validatedItems.reduce((sum, i) => sum + i.subtotal, 0); const order = await Order.create({ userId: req.user.id, items: validatedItems, total }); res.status(201).json(order); }); // ❌ Missing data integrity checks (race conditions) // Two simultaneous requests both read balance=100, both deduct 50 // Final balance: 50 instead of 0 (should be -100 which should be rejected!) // ✅ Database transactions with proper isolation async function transferFunds(fromId, toId, amount) { const result = await db.transaction(async (trx) => { // Acquire row-level lock (SELECT FOR UPDATE prevents concurrent reads) const fromAccount = await trx('accounts') .where('id', fromId) .forUpdate() // Row lock! .first(); if (fromAccount.balance < amount) { throw new Error('Insufficient funds'); } await trx('accounts') .where('id', fromId) .decrement('balance', amount); await trx('accounts') .where('id', toId) .increment('balance', amount); return { success: true }; }); return result; } Quick Security Checklist Before deploying any application: Authentication & Authorization ☐ Passwords hashed with bcrypt/scrypt/Argon2 (min 12 rounds) ☐ JWT access tokens expire in ≤15 minutes ☐ Refresh tokens stored securely, revocable ☐ Every API endpoint checks authorization ☐ Rate limiting on all auth endpoints ☐ Account lockout after failed attempts Input Validation ☐ All inputs validated server-side (not just client-side) ☐ SQL: parameterized queries always ☐ NoSQL: operator sanitization / strict schema ☐ Shell: allowlist validation for command args ☐ File uploads: type + size limits + virus scan Data Protection ☐ HTTPS everywhere (HSTS enabled) ☐ Sensitive data encrypted at rest ☐ PII fields identified and protected ☐ Logs don't contain sensitive data ☐ Database transactions for financial operations Infrastructure ☐ Security headers set (Helmet/CSP) ☐ Error messages generic in production ☐ Dependencies scanned (npm audit) ☐ Debug endpoints disabled in production ☐ CORS configured correctly (not '*') Monitoring ☐ Failed login attempt alerts ☐ Unusual traffic pattern detection ☐ Security event logging (who did what when) ☐ Incident response plan documented What's the most common security issue YOU see in codebases? What tools do you use for security scanning? Follow @armorbreak for more practical developer guides.
6 days agoWeb Security: OWASP Top 10 and How to Fix Them (2026) Security isn't a feature you add later — it's built into every layer. Here's how the top 10 vulnerabilities work and how to prevent them. #1 Broken Access Control // ❌ Vulnerable: User can access anyone's data app.get('/api/users/:id', (req, res) => { const user = await db.users.findById(req.params.id); res.json(user); // No check if requester owns this data! }); // ✅ Secure: Always verify ownership app.get('/api/users/:id', async (req, res) => { // Check: Is the logged-in user requesting their OWN data? if (req.params.id !== req.user.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Access denied' }); } const user = await db.users.findById(req.params.id); res.json(user); }); // ✅ Better: Use middleware for all protected routes const requireOwnership = (resourceType) => async (req, res, next) => { const resource = await db[resourceType].findById(req.params.id); if (!resource) return res.status(404).json({ error: 'Not found' }); if (resource.userId !== req.user.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Access denied' }); } req.resource = resource; // Attach for route handler next(); }; app.get('/api/posts/:id', auth, requireOwnership('posts'), (req, res) => { res.json(req.resource); }); #2 Cryptographic Failures // ❌ Storing passwords in plain text or weak hashing const password = "password123"; db.users.insert({ email, password }); // NEVER DO THIS! // ✅ Proper password hashing with bcrypt const bcrypt = require('bcrypt'); const SALT_ROUNDS = 12; // Higher = slower = more secure (12 is good balance) async function hashPassword(password) { return bcrypt.hash(password, SALT_ROUNDS); } async function comparePassword(password, hash) { return bcrypt.compare(password, hash); // Handles salt automatically } // Usage: const hashedPassword = await hashPassword("user_password"); // $2b$12$N9qo8uLOickG2SODUUS... (60 chars, includes salt + hash) const isValid = await comparePassword(input, hashedPassword); // ❌ Using MD5/SHA1 for passwords (too fast, vulnerable to rainbow tables) const md5Hash = crypto.createHash('md5').update(password).digest('hex'); // Cracked in milliseconds with rainbow tables // ✅ For data encryption (not passwords!): const crypto = require('crypto'); function encrypt(text, key) { const iv = crypto.randomBytes(16); // Unique IV per encryption! const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key), iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') }; } function decrypt(encryptedData, key) { const decipher = crypto.createDecipheriv( 'aes-256-gcm', Buffer.from(key), Buffer.from(encryptedData.iv, 'hex') ); decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex')); let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // ⚠️ Key management: // - Never hardcode keys in source code! // - Use environment variables or secrets manager (AWS KMS, HashiCorp Vault) // - Rotate keys regularly #3 Injection (SQL, NoSQL, Command) // === SQL Injection === // ❌ String concatenation in queries app.get('/search', (req, res) => { db.query(`SELECT * FROM products WHERE name LIKE '%${req.query.q}%'`); // Attacker sends q = "'; DROP TABLE users; --" // Result: SELECT * FROM products WHERE name LIKE ''; DROP TABLE users; --' }); // ✅ Parameterized queries (always!) app.get('/search', async (req, res) => { const results = await db.query( 'SELECT * FROM products WHERE name LIKE ?', [`%${req.query.q}%`] // Safely escaped by driver ); res.json(results); }); // With ORM (even safer): const results = await Product.findAll({ where: { name: { [Op.like]: `%${req.query.q}%` } } }); // === NoSQL Injection === // ❌ Passing user input directly to MongoDB query app.post('/login', async (req, res) => { const user = await db.collection('users').findOne({ username: req.body.username, password: req.body.password }); // Attacker sends: username: {"$gt": ""}, password: {"$gt": ""} // Matches ANY document! // ✅ Use strict equality checks const user = await db.collection('users').findOne({ username: req.body.username, password: req.body.password }, { // Disable operators that could be exploited sanitizeFilter: true // MongoDB option }); // Or use a library like mongo-sanitize: const sanitize = require('mongo-sanitize'); const cleanInput = sanitize(req.body); }); // === Command Injection === // ❌ Running shell commands with user input const { exec } = require('child_process'); app.get('/ping', (req, res) => { exec(`ping -c 4 ${req.body.ip}`, (err, stdout) => { res.send(stdout); }); // Attacker sends ip = "127.0.0.1; rm -rf /" // Executes BOTH commands! }); // ✅ Validate input strictly (allowlist approach) const VALID_IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/; app.get('/ping', (req, res) => { if (!VALID_IP_REGEX.test(req.body.ip)) { return res.status(400).json({ error: 'Invalid IP address' }); } exec(`ping -c 4 ${req.body.ip}`, (err, stdout) => { res.send(stdout); }); }); #4 Insecure Design // ❌ Design flaw: Password reset token is predictable function generateResetToken() { return Math.random().toString(36).substring(7); // Weak randomness! } // ✅ Design fix: Use cryptographically secure random tokens const crypto = require('crypto'); function generateResetToken() { return crypto.randomBytes(32).toString('hex'); // 64 hex chars, unpredictable } // Store with expiry: await db.passwordResets.insert({ userId, token: generateResetToken(), expiresAt: Date.now() + 15 * 60 * 1000, // 15 minutes used: false }); // ❌ Design flaw: Rate limiting on client side only // <button onclick="submitForm()">Submit</button> // <script>let clicks=0; function submitForm(){if(clicks<5){clicks++; ...}}</script> // Attacker just ignores JavaScript and sends unlimited requests. // ✅ Server-side rate limiting: const rateLimit = require('express-rate-limit'); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Max 5 attempts per IP message: { error: 'Too many attempts. Try again in 15 minutes.' }, standardHeaders: true, legacyHeaders: false, }); app.post('/api/login', loginLimiter, handleLogin); // ❌ Design flaw: No account lockout after failed attempts // Brute force can try billions of combinations over time // ✅ Account lockout mechanism: async function handleFailedLogin(userId) { const attempts = await incrementLoginAttempts(userId); if (attempts >= 5) { await lockAccount(userId, 30 * 60 * 1000); // Lock for 30 minutes sendSecurityEmail(userId, 'Account locked due to suspicious activity'); } } #5 Security Misconfiguration // ❌ Exposing stack traces in production app.use((err, req, res, next) => { res.status(500).json({ error: err.message, stack: err.stack }); // Reveals file paths, library versions, internal structure! // ✅ Generic error in production, details in logs const isDev = process.env.NODE_ENV !== 'production'; res.status(err.statusCode || 500).json({ error: isDev ? err.message : 'Internal server error', ...(isDev && { stack: err.stack }), incidentId: generateIncidentId(), // For support lookup }); logger.error(`[${incidentId}]`, err.stack); // Details go to logs only }); // ❌ Missing security headers // Without headers: Clickjacking possible, MIME sniffing, etc. // ✅ Add security headers with Helmet: const helmet = require('helmet'); app.use(helmet()); // Adds: // X-Frame-Options: DENY (prevents clickjacking) // X-Content-Type-Options: nosniff (prevents MIME sniffing) // X-XSS-Protection: mode=block // Referrer-Policy: strict-origin-when-cross-origin // Content-Security-Policy (configurable) // Strict-Transport-Security (HTTPS enforcement) // Custom CSP configuration: app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "cdn.example.com"], styleSrc: ["'self'", "'unsafe-inline'"], // Needed for some CSS frameworks imgSrc: ["'self'", "data:", "https:"], fontSrc: ["'self'"], connectSrc: ["'self'", "api.example.com"], frameAncestors: ["'none'"], // Prevents clickjacking formAction: ["'self'"], upgradeInsecureRequests: [], // Force HTTPS }, })); // ❌ Debug endpoints left enabled in production if (process.env.DEBUG) { app.get('/debug/routes', (req, res) => res.json(app._router.stack)); app.get('/debug/env', (req, res) => res.json(process.env)); } // Forgets to disable DEBUG in production → leaks everything! // ✅ Environment-based feature flags: const debugRoutes = express.Router(); debugRoutes.get('/routes', (req, res) => res.json(app._router.stack)); if (process.env.NODE_ENV === 'development') { app.use('/debug', debugRoutes); } // Production simply doesn't mount these routes at all. #6 Vulnerable & Outdated Components # The problem: Dependencies have known vulnerabilities # Attackers scan for outdated packages with public CVEs # Check your dependencies: npm audit # Shows known vulnerabilities npm audit --fix # Auto-fixes where possible (patch/minor updates) # Automated scanning in CI: # .github/workflows/security.yml name: Security Audit on: [push, pull_request] jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm audit --audit-level=moderate # Fails CI if moderate+ severity vulns found # Lockfile integrity: npm ci # Uses package-lock.json exactly (reproducible installs) # vs npm install # May update versions (unpredictable!) # Regular dependency updates: # Use Dependabot (GitHub native), Renovate, or Snyk # Set up weekly PRs for dependency updates #7 Authentication Failures // ❌ Weak session management app.post('/login', (req, res) => { const token = jwt.sign({ userId: user.id }, SECRET_KEY); res.json({ token }); // Never expires! }); // ✅ Secure JWT implementation const jwt = require('jsonwebtoken'); function generateTokens(user) { const accessToken = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' } // Short-lived access token ); const refreshToken = crypto.randomBytes(40).toString('hex'); // Store refresh token in DB with expiry await db.refreshTokens.insert({ token: refreshToken, userId: user.id, expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days }); return { accessToken, refreshToken }; } // Token refresh endpoint: app.post('/refresh', async (req, res) => { const { refreshToken } = req.body; const stored = await db.refreshTokens.findOne({ token: refreshToken }); if (!stored || stored.expiresAt < Date.now() || stored.revoked) { return res.status(401).json({ error: 'Invalid or expired refresh token' }); } const user = await db.users.findById(stored.userId); const tokens = generateTokens(user); // Revoke old refresh token await db.refreshTokens.update(stored.id, { revoked: true }); res.json(tokens); }); // ❌ Allowing weak passwords if (password.length >= 6) accept(); // Too short! // ✅ Password strength requirements: function validatePasswordStrength(password) { const issues = []; if (password.length < 12) issues.push('At least 12 characters'); if (!/[a-z]/.test(password)) issues.push('One lowercase letter'); if (!/[A-Z]/.test(password)) issues.push('One uppercase letter'); if (!/\d/.test(password)) issues.push('One number'); if (!/[!@#$%^&*]/.test(password)) issues.push('One special character'); // Also check against common/breached passwords if (COMMON_PASSWORDS.includes(password.toLowerCase())) { issues.push('This password is too common'); } return issues.length === 0 ? null : issues; } #8 Data Integrity Failures // ❌ Trusting client-side data blindly app.post('/api/orders', (req, res) => { const { total, items } = req.body; // Client sent total = $0.01 for $1000 worth of items! createOrder(total, items); }); // ✅ Server-side validation and recalculation app.post('/api/orders', auth, async (req, res) => { const { items } = req.body; // Validate each item exists and price is current const validatedItems = []; for (const item of items) { const product = await Product.findById(item.productId); if (!product || !product.active) { return res.status(400).json({ error: `Invalid product: ${item.productId}` }); } // Recalculate price from DB (never trust client's price!) validatedItems.push({ productId: item.productId, quantity: Math.min(item.quantity, product.maxOrderQuantity), unitPrice: product.price, // From database, not request! subtotal: product.price * Math.min(item.quantity, product.maxOrderQuantity) }); } const total = validatedItems.reduce((sum, i) => sum + i.subtotal, 0); const order = await Order.create({ userId: req.user.id, items: validatedItems, total }); res.status(201).json(order); }); // ❌ Missing data integrity checks (race conditions) // Two simultaneous requests both read balance=100, both deduct 50 // Final balance: 50 instead of 0 (should be -100 which should be rejected!) // ✅ Database transactions with proper isolation async function transferFunds(fromId, toId, amount) { const result = await db.transaction(async (trx) => { // Acquire row-level lock (SELECT FOR UPDATE prevents concurrent reads) const fromAccount = await trx('accounts') .where('id', fromId) .forUpdate() // Row lock! .first(); if (fromAccount.balance < amount) { throw new Error('Insufficient funds'); } await trx('accounts') .where('id', fromId) .decrement('balance', amount); await trx('accounts') .where('id', toId) .increment('balance', amount); return { success: true }; }); return result; } Quick Security Checklist Before deploying any application: Authentication & Authorization ☐ Passwords hashed with bcrypt/scrypt/Argon2 (min 12 rounds) ☐ JWT access tokens expire in ≤15 minutes ☐ Refresh tokens stored securely, revocable ☐ Every API endpoint checks authorization ☐ Rate limiting on all auth endpoints ☐ Account lockout after failed attempts Input Validation ☐ All inputs validated server-side (not just client-side) ☐ SQL: parameterized queries always ☐ NoSQL: operator sanitization / strict schema ☐ Shell: allowlist validation for command args ☐ File uploads: type + size limits + virus scan Data Protection ☐ HTTPS everywhere (HSTS enabled) ☐ Sensitive data encrypted at rest ☐ PII fields identified and protected ☐ Logs don't contain sensitive data ☐ Database transactions for financial operations Infrastructure ☐ Security headers set (Helmet/CSP) ☐ Error messages generic in production ☐ Dependencies scanned (npm audit) ☐ Debug endpoints disabled in production ☐ CORS configured correctly (not '*') Monitoring ☐ Failed login attempt alerts ☐ Unusual traffic pattern detection ☐ Security event logging (who did what when) ☐ Incident response plan documented What's the most common security issue YOU see in codebases? What tools do you use for security scanning? Follow @armorbreak for more practical developer guides.
6 days ago
Court reserves ruling on Blessing CEO’s bail amid multiple EFCC fraud cases
A Federal High Court in Ikoyi, Lagos reserves ruling on a bail application by social media influencer Okoro Blessing Nki...
FIFA World Cup tickets: prices rise and availability varies under dynamic pricing
FIFA is selling tickets for the World Cup using dynamic pricing, meaning prices can change based on demand. Multiple out...
England and Scotland drawn into groups for 2026 FIFA World Cup
The full group stage matchups for the 48-team FIFA World Cup 2026 are set for the tournament taking place across Mexico,...