Web Security - Complete Guide

Web security is critical for protecting applications and users. This guide covers essential security concepts, common vulnerabilities, and best practices.

OWASP Top 10 #

The Open Web Application Security Project lists the most critical security risks.

1. Injection Attacks #

SQL Injection #

Attackers insert malicious SQL code.

// Vulnerable
app.get('/user', (req, res) => {
  const query = `SELECT * FROM users WHERE id = ${req.query.id}`;
  db.query(query);  // Dangerous!
});

// Safe - Use parameterized queries
app.get('/user', (req, res) => {
  const query = 'SELECT * FROM users WHERE id = ?';
  db.query(query, [req.query.id]);
});

Prevention:

  • Use parameterized queries
  • Use ORMs with proper escaping
  • Validate and sanitize input
  • Use least privilege database accounts

NoSQL Injection #

// Vulnerable
db.users.find({ username: req.body.username });

// If input is: {"$ne": null}
// Query becomes: find({ username: {"$ne": null} })

// Safe
const username = String(req.body.username);
db.users.find({ username: username });

2. Cross-Site Scripting (XSS) #

Inject malicious scripts into web pages.

Reflected XSS #

// Vulnerable
app.get('/search', (req, res) => {
  res.send(`Results for: ${req.query.q}`);
});

// Attack: /search?q=<script>alert('XSS')</script>

// Safe - Escape output
app.get('/search', (req, res) => {
  const escaped = escapeHtml(req.query.q);
  res.send(`Results for: ${escaped}`);
});

Stored XSS #

// Vulnerable
app.post('/comment', (req, res) => {
  const comment = req.body.text;
  db.save({ text: comment });  // Stores malicious script
});

// Safe - Sanitize before storing
const DOMPurify = require('isomorphic-dompurify');

app.post('/comment', (req, res) => {
  const clean = DOMPurify.sanitize(req.body.text);
  db.save({ text: clean });
});

DOM XSS #

// Vulnerable
const search = window.location.search;
document.getElementById('result').innerHTML = search;

// Safe
document.getElementById('result').textContent = search;

Prevention:

  • Escape output based on context (HTML, JavaScript, CSS, URL)
  • Use Content Security Policy (CSP)
  • Sanitize user input
  • Use textContent instead of innerHTML
  • Use frameworks that auto-escape (React, Vue)

3. Cross-Site Request Forgery (CSRF) #

Force users to execute unwanted actions.

// Vulnerable
app.post('/transfer', (req, res) => {
  const { to, amount } = req.body;
  transfer(req.user.id, to, amount);
});

// Attack via malicious site:
// <form action="https://bank.com/transfer" method="POST">
//   <input name="to" value="attacker">
//   <input name="amount" value="1000">
// </form>

// Safe - Use CSRF tokens
const csrf = require('csurf');
app.use(csrf());

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

app.post('/transfer', (req, res) => {
  // Token automatically validated
  transfer(req.user.id, req.body.to, req.body.amount);
});

Prevention:

  • Use CSRF tokens
  • Check Referer header
  • Use SameSite cookie attribute
  • Require re-authentication for sensitive actions

4. Authentication Issues #

Weak Passwords #

const bcrypt = require('bcrypt');

// Hash passwords
async function createUser(username, password) {
  const hash = await bcrypt.hash(password, 10);
  await db.users.insert({ username, password: hash });
}

// Verify passwords
async function login(username, password) {
  const user = await db.users.findOne({ username });
  const valid = await bcrypt.compare(password, user.password);
  return valid;
}

Session Management #

const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,      // HTTPS only
    httpOnly: true,    // Not accessible via JavaScript
    maxAge: 3600000,   // 1 hour
    sameSite: 'strict'
  }
}));

Prevention:

  • Hash passwords with bcrypt, argon2, or scrypt
  • Enforce strong password policies
  • Implement rate limiting
  • Use MFA (Multi-Factor Authentication)
  • Secure session management
  • Implement account lockout

5. Sensitive Data Exposure #

// Vulnerable
app.get('/api/user', (req, res) => {
  const user = await db.users.findById(req.userId);
  res.json(user);  // Sends password hash, etc.
});

// Safe - Filter sensitive data
app.get('/api/user', (req, res) => {
  const user = await db.users.findById(req.userId);
  const { password, ssn, ...safe } = user;
  res.json(safe);
});

Prevention:

  • Use HTTPS everywhere
  • Encrypt sensitive data at rest
  • Don’t store unnecessary sensitive data
  • Use secure key management
  • Implement proper access controls

6. XML External Entities (XXE) #

const libxmljs = require('libxmljs');

// Vulnerable
const xml = libxmljs.parseXml(userInput);

// Safe - Disable external entities
const xml = libxmljs.parseXml(userInput, {
  noent: false,
  dtdload: false,
  dtdvalid: false
});

7. Broken Access Control #

// Vulnerable
app.get('/api/user/:id', (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(user);  // Any user can access any profile
});

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

Prevention:

  • Deny by default
  • Implement proper authorization checks
  • Don’t rely on client-side access control
  • Use object-level permissions
  • Log access control failures

8. Security Misconfiguration #

// Vulnerable
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.stack });
});

// Safe
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

Prevention:

  • Remove default credentials
  • Disable directory listing
  • Keep software updated
  • Configure security headers
  • Remove unnecessary features
  • Don’t expose stack traces in production

9. Using Components with Known Vulnerabilities #

# Check for vulnerabilities
npm audit

# Fix vulnerabilities
npm audit fix

# Monitor dependencies
npm install -g snyk
snyk test

10. Insufficient Logging & Monitoring #

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Log security events
app.post('/login', async (req, res) => {
  const success = await attemptLogin(req.body);

  if (!success) {
    logger.warn('Failed login attempt', {
      username: req.body.username,
      ip: req.ip,
      timestamp: new Date()
    });
  }
});

Security Headers #

const helmet = require('helmet');

app.use(helmet());

// Or configure individually
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:']
  }
}));

app.use(helmet.hsts({
  maxAge: 31536000,
  includeSubDomains: true,
  preload: true
}));

app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.xssFilter());

Important Headers #

Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: no-referrer
Permissions-Policy: geolocation=(), microphone=()

Input Validation #

const validator = require('validator');

// Email validation
function validateEmail(email) {
  return validator.isEmail(email);
}

// URL validation
function validateURL(url) {
  return validator.isURL(url, {
    protocols: ['http', 'https'],
    require_protocol: true
  });
}

// Sanitize input
function sanitizeInput(input) {
  return validator.escape(validator.trim(input));
}

// Complex validation
const { body, validationResult } = require('express-validator');

app.post('/register',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  body('username').isAlphanumeric().trim(),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Proceed with registration
  }
);

Rate Limiting #

const rateLimit = require('express-rate-limit');

// General rate limit
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100  // Limit each IP to 100 requests per windowMs
});

app.use(limiter);

// Strict limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: 'Too many login attempts'
});

app.post('/login', authLimiter, (req, res) => {
  // Login logic
});

API Security #

JWT Best Practices #

const jwt = require('jsonwebtoken');

// Create token
function createToken(userId) {
  return jwt.sign(
    { userId },
    process.env.JWT_SECRET,
    {
      expiresIn: '1h',
      algorithm: 'HS256'
    }
  );
}

// Verify token
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (err) {
    return null;
  }
}

// Middleware
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const decoded = verifyToken(token);
  if (!decoded) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  req.userId = decoded.userId;
  next();
}

API Keys #

const crypto = require('crypto');

// Generate API key
function generateApiKey() {
  return crypto.randomBytes(32).toString('hex');
}

// Store hashed
const hash = crypto
  .createHash('sha256')
  .update(apiKey)
  .digest('hex');

// Verify API key middleware
async function verifyApiKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey) {
    return res.status(401).json({ error: 'No API key' });
  }

  const hash = crypto
    .createHash('sha256')
    .update(apiKey)
    .digest('hex');

  const valid = await db.apiKeys.findOne({ hash });

  if (!valid) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  next();
}

File Upload Security #

const multer = require('multer');
const path = require('path');

const upload = multer({
  storage: multer.diskStorage({
    destination: 'uploads/',
    filename: (req, file, cb) => {
      // Generate random filename
      const randomName = crypto.randomBytes(16).toString('hex');
      cb(null, randomName + path.extname(file.originalname));
    }
  }),
  limits: {
    fileSize: 5 * 1024 * 1024  // 5MB
  },
  fileFilter: (req, file, cb) => {
    // Allow only specific types
    const allowed = ['.jpg', '.jpeg', '.png', '.pdf'];
    const ext = path.extname(file.originalname).toLowerCase();

    if (allowed.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  }
});

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ filename: req.file.filename });
});

Database Security #

// Use parameterized queries
const userId = req.params.id;
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);

// MongoDB - Avoid $where
// Bad
db.users.find({ $where: `this.username == '${username}'` });

// Good
db.users.find({ username: username });

// Principle of least privilege
// Create read-only user for queries
CREATE USER 'readonly'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON database.* TO 'readonly'@'localhost';

Encryption #

const crypto = require('crypto');

// Encrypt data
function encrypt(text, key) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);

  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  return iv.toString('hex') + ':' + encrypted;
}

// Decrypt data
function decrypt(encrypted, key) {
  const parts = encrypted.split(':');
  const iv = Buffer.from(parts[0], 'hex');
  const encryptedText = parts[1];

  const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);

  let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

CORS Security #

const cors = require('cors');

// Restrictive CORS
app.use(cors({
  origin: ['https://trusted-domain.com'],
  credentials: true,
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Dynamic origin validation
app.use(cors({
  origin: (origin, callback) => {
    const allowedOrigins = ['https://app1.com', 'https://app2.com'];

    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

Environment Variables #

// .env file
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
API_KEY=your-api-key

// Load securely
require('dotenv').config();

const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;

// Never commit .env to git
// Add to .gitignore:
# .env
# .env.local

Security Checklist #

Authentication #

  • Hash passwords with bcrypt/argon2
  • Implement rate limiting on login
  • Use secure session management
  • Implement MFA
  • Enforce strong password policy

Authorization #

  • Check permissions on every request
  • Use role-based access control
  • Implement object-level permissions
  • Default deny access

Data Protection #

  • Use HTTPS everywhere
  • Encrypt sensitive data at rest
  • Use secure headers (Helmet)
  • Implement CSP
  • Sanitize user input

API Security #

  • Validate all inputs
  • Use rate limiting
  • Implement CORS properly
  • Use authentication (JWT/OAuth)
  • Version your API

Infrastructure #

  • Keep dependencies updated
  • Run security audits (npm audit)
  • Use environment variables
  • Implement logging and monitoring
  • Regular security testing

Common Vulnerabilities #

Path Traversal #

// Vulnerable
app.get('/file', (req, res) => {
  const file = req.query.name;
  res.sendFile(`/uploads/${file}`);
});

// Attack: /file?name=../../../etc/passwd

// Safe
const path = require('path');

app.get('/file', (req, res) => {
  const safePath = path.normalize(req.query.name).replace(/^(\.\.(\/|\\|$))+/, '');
  const fullPath = path.join('/uploads', safePath);

  if (!fullPath.startsWith('/uploads')) {
    return res.status(400).send('Invalid path');
  }

  res.sendFile(fullPath);
});

Command Injection #

// Vulnerable
const { exec } = require('child_process');

app.get('/ping', (req, res) => {
  exec(`ping -c 4 ${req.query.host}`, (error, stdout) => {
    res.send(stdout);
  });
});

// Attack: /ping?host=google.com; rm -rf /

// Safe - Don't use exec with user input
// Use libraries or validate heavily
const { spawn } = require('child_process');

app.get('/ping', (req, res) => {
  const host = req.query.host;

  // Validate input
  if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
    return res.status(400).send('Invalid host');
  }

  const ping = spawn('ping', ['-c', '4', host]);

  let output = '';
  ping.stdout.on('data', data => output += data);
  ping.on('close', () => res.send(output));
});

Open Redirect #

// Vulnerable
app.get('/redirect', (req, res) => {
  res.redirect(req.query.url);
});

// Attack: /redirect?url=https://malicious-site.com

// Safe - Whitelist domains
app.get('/redirect', (req, res) => {
  const allowed = [
    'https://mysite.com',
    'https://trusted.com'
  ];

  const url = req.query.url;

  if (allowed.some(domain => url.startsWith(domain))) {
    res.redirect(url);
  } else {
    res.status(400).send('Invalid redirect URL');
  }
});

Best Practices #

  1. Defense in depth - Multiple security layers
  2. Principle of least privilege - Minimal necessary access
  3. Fail securely - Default to secure state
  4. Keep it simple - Complex = more vulnerabilities
  5. Don’t trust user input - Validate everything
  6. Security by design - Not an afterthought
  7. Stay updated - Monitor security advisories
  8. Regular audits - Test your security
  9. Educate team - Security is everyone’s responsibility
  10. Plan for breaches - Incident response plan

Web security requires constant vigilance. Stay informed about new vulnerabilities and always follow security best practices.