DEV Community

Alex Chen
Alex Chen

Posted on

Web Security Basics Every Developer Must Know

Web Security Basics Every Developer Must Know

Your app works. But is it safe? Here's what you need to know.

1. SQL Injection

// ❌ Vulnerable — attacker can inject SQL
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query);

// Input: userId = "1 OR 1=1" → Returns ALL users!
// Input: userId = "1; DROP TABLE users;" → Data deleted!

// ✅ Safe — parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);

// Or with an ORM (even safer)
const user = await User.findByPk(userId);
Enter fullscreen mode Exit fullscreen mode

2. XSS (Cross-Site Scripting)

// ❌ Vulnerable — renders untrusted HTML
div.innerHTML = userComment;
// Attacker input: <script>stealCookies()</script>
// or: <img src=x onerror="stealCookies()">

// ✅ Safe — escape all output
div.textContent = userComment; // Text only, no HTML parsing
div.innerText = userComment;   // Same as textContent

// If you NEED HTML (rich text editor):
import DOMPurify from 'dompurify';
div.innerHTML = DOMPurify.sanitize(userComment); // Removes dangerous tags/attributes

// In React/Vue/Angular, JSX templates auto-escape:
// <div>{userComment}</div> // Safe by default!
// dangerouslySetInnerHTML={{ __html: userComment }} // DANGEROUS! Only with sanitization
Enter fullscreen mode Exit fullscreen mode

3. CSRF (Cross-Site Request Forgery)

// Attack: User visits evil.com while logged into your site
// evil.com has: <img src="https://your-site.com/transfer?to=attacker&amount=1000">
// Browser automatically sends cookies → transfer executed!

// Defense: CSRF tokens + SameSite cookies

// Express + csurf middleware:
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/transfer', csrfProtection, (req, res) => {
  // Token validated automatically
  processTransfer(req.body);
});

// Cookie defense:
app.use(session({
  cookie: {
    sameSite: 'strict', // Don't send cookies to other sites
    secure: true,       // Only over HTTPS
    httpOnly: true,     // JavaScript can't read the cookie
  }
}));
Enter fullscreen mode Exit fullscreen mode

4. Authentication Security

// ❌ Storing passwords in plain text
user.password = 'password123'; // NEVER!

// ✅ Hashing with bcrypt
import bcrypt from 'bcryptjs';

const SALT_ROUNDS = 12;

// Register
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
await User.create({ email, password: hashedPassword });

// Login
const user = await User.findOne({ where: { email } });
if (!user) return error('User not found');

const isValid = await bcrypt.compare(password, user.password);
if (!isValid) return error('Invalid password');

// JWT tokens (stateless auth)
import jwt from 'jsonwebtoken';

const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '7d' }
);

// Validate token
try {
  const payload = jwt.verify(token, process.env.JWT_SECRET);
  req.user = payload; // Attach to request
} catch (err) {
  return res.status(401).json({ error: 'Invalid token' });
}
Enter fullscreen mode Exit fullscreen mode

5. Rate Limiting

// Without rate limiting:
// - Brute force login attempts
// - API abuse / scraping
// - DDoS amplification

import rateLimit from 'express-rate-limit';

// General API limiter
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                 // 100 requests per IP
  message: { error: 'Too many requests' },
  standardHeaders: true,
});

app.use('/api/', apiLimiter);

// Stricter for login endpoint
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,                   // Only 5 attempts per 15 min
  message: { error: 'Too many login attempts. Try again later.' },
  skipSuccessfulRequests: true, // Don't count successful logins
});

app.post('/api/login', loginLimiter, handleLogin);
Enter fullscreen mode Exit fullscreen mode

6. Input Validation

import { z } from 'zod';

const registerSchema = z.object({
  email: z.string().email('Invalid email').max(255),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Must contain uppercase letter')
    .regex(/[a-z]/, 'Must contain lowercase letter')
    .regex(/[0-9]/, 'Must contain a number'),
  name: z.string().min(1).max(100).trim(),
  age: z.number().int().min(18).max(150).optional(),
});

// Validate before processing
function register(req, res) {
  const result = registerSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({ 
      error: 'Validation failed', 
      details: result.error.flatten() 
    });
  }
  // Process valid data...
}
Enter fullscreen mode Exit fullscreen mode

7. HTTP Security Headers

import helmet from 'helmet';
app.use(helmet());

// What helmet adds:
// X-Content-Type-Options: nosniff        // Prevent MIME sniffing
// X-Frame-Options: SAMEORIGIN             // Prevent clickjacking
// X-XSS-Protection: 0                      // Legacy XSS filter
// Referrer-Policy: strict-origin-when-cross-origin
// Permissions-Policy: ...                  // Control browser features
// Content-Security-Policy: ...             // Control resource loading

// Custom CSP (most powerful header):
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"], // Remove unsafe-inline in production!
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    connectSrc: ["'self'", "https://api.example.com"],
    frameAncestors: ["'none'"],
  },
}));
Enter fullscreen mode Exit fullscreen mode

8. CORS Configuration

import cors from 'cors';

// ❌ Too permissive
app.use(cors()); // Allows any origin!

// ✅ Proper configuration
app.use(cors({
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://myapp.com',
      'https://www.myapp.com',
    ];

    // Allow in development
    if (process.env.NODE_ENV === 'development') {
      return callback(null, true);
    }

    if (!origin) return callback(null, true); // Mobile apps, Postman

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,          // Allow cookies/auth headers
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,              // Preflight cache for 24 hours
}));
Enter fullscreen mode Exit fullscreen mode

Security Checklist

Before deploying to production:

Authentication & Authz
[ ] Passwords hashed (bcrypt/argon2)
[ ] JWT secrets are strong (32+ random chars)
[ ] Tokens have reasonable expiry
[ ] Role-based access control implemented
[ ] Sessions use secure, httpOnly, sameSite cookies

Input Handling
[ ] All inputs validated (server-side!)
[ ] SQL uses parameterized queries / ORM
[ ] Output is escaped/sanitized
[ ] File uploads validate type AND content
[ ] Rate limiting on all public endpoints

HTTP Security
[ ] Helmet installed and configured
[ ] CSP header set appropriately
[ ] CORS restricted to known origins
[ ] HTTPS enforced (redirect HTTP)
[ ] HSTS enabled

Data Protection
[ ] No sensitive data in URLs or logs
[ ] PII encrypted at rest
[ ] API keys not exposed to client
[ ] Error messages don't leak internals
[ ] Secrets in env vars (never in code!)

Infrastructure
[ ] Dependencies up to date (npm audit)
[ ] Environment variables configured correctly
[ ] Firewall rules restrictive
[ ] Access logs enabled and monitored
[ ] Backup strategy in place
Enter fullscreen mode Exit fullscreen mode

What's the most important security lesson you've learned?

Follow @armorbreak for more web development content.

Top comments (0)