Web Security Fundamentals

Web Security Fundamentals

Learn essential web security practices to protect your JavaScript applications and users.

Why Web Security Matters

Web security protects your application, data, and users from malicious attacks. A single vulnerability can lead to data breaches, financial loss, and damage to your reputation.

Common Web Security Threats

Cross-Site Scripting (XSS)

XSS attacks inject malicious scripts into web pages viewed by other users.

Types of XSS:

  • Stored XSS: Malicious script stored in database
  • Reflected XSS: Script reflected in server response
  • DOM-based XSS: Script executes in browser DOM

Prevention:

// Bad - vulnerable to XSS
element.innerHTML = userInput;

// Good - escape HTML
function escapeHtml(text) {
  const map = {
    '&': '&',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  };
  return text.replace(/[&<>"']/g, m => map[m]);
}

element.textContent = userInput;

// Better - use textContent instead of innerHTML
element.textContent = userInput;

// Best - use DOMPurify library
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

SQL Injection

SQL injection occurs when malicious SQL code is inserted into user input.

Prevention with Parameterized Queries:

// Bad - vulnerable to SQL injection
const query = `SELECT * FROM users WHERE email = '${email}'`;

// Good - parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email]);

// Using ORM (like Sequelize)
const user = await User.findOne({ where: { email } });

Cross-Site Request Forgery (CSRF)

CSRF tricks authenticated users into performing unwanted actions.

Prevention with CSRF Tokens:

// Express.js with csurf middleware
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.get('/api/user', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Include token in forms
app.post('/api/update', csrfProtection, (req, res) => {
  // Protected route
});

Authentication & Authorization

Password Security

Hashing Passwords:

const bcrypt = require('bcrypt');

// Hash password
async function hashPassword(password) {
  const saltRounds = 10;
  return await bcrypt.hash(password, saltRounds);
}

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

// Usage
const hashedPassword = await hashPassword(userPassword);
const isValid = await verifyPassword(inputPassword, storedHash);

JWT Authentication

const jwt = require('jsonwebtoken');

// Generate token
function generateToken(user) {
  return jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );
}

// Verify token
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (error) {
    throw new Error('Invalid token');
  }
}

// Middleware for protected routes
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

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

  try {
    const decoded = verifyToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}

Session Management

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

app.use(expressSession({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only
    httpOnly: true, // Prevent XSS
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

Input Validation

Server-Side Validation

const Joi = require('joi');

// Validation schema
const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
  age: Joi.number().integer().min(13).max(120)
});

// Validation middleware
function validateUser(req, res, next) {
  const { error } = userSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ 
      error: 'Validation failed', 
      details: error.details 
    });
  }
  next();
}

// Usage
app.post('/api/users', validateUser, (req, res) => {
  // Process valid data
});

Client-Side Validation

// Email validation
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// Password strength validation
function isStrongPassword(password) {
  const minLength = 8;
  const hasUpperCase = /[A-Z]/.test(password);
  const hasLowerCase = /[a-z]/.test(password);
  const hasNumbers = /\d/.test(password);
  const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);

  return password.length >= minLength &&
         hasUpperCase &&
         hasLowerCase &&
         hasNumbers &&
         hasSpecialChar;
}

Secure Headers

Using Helmet.js

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

Important Security Headers

HeaderPurpose
X-Frame-OptionsPrevent clickjacking
X-Content-Type-OptionsPrevent MIME sniffing
X-XSS-ProtectionEnable XSS filtering
Strict-Transport-SecurityEnforce HTTPS
Content-Security-PolicyControl resource loading

HTTPS and SSL

Enforcing HTTPS

// Force HTTPS in production
function requireHTTPS(req, res, next) {
  if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
    return res.redirect(301, `https://${req.get('host')}${req.url}`);
  }
  next();
}

if (process.env.NODE_ENV === 'production') {
  app.use(requireHTTPS);
}

SSL Configuration

const https = require('https');
const fs = require('fs');

// For development only
const options = {
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.cert')
};

https.createServer(options, app).listen(443);

Data Protection

Encrypting Sensitive Data

const crypto = require('crypto');

// Encryption
function encrypt(text, secretKey) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipher('aes-256-cbc', secretKey);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return iv.toString('hex') + ':' + encrypted;
}

// Decryption
function decrypt(text, secretKey) {
  const textParts = text.split(':');
  const iv = Buffer.from(textParts.shift(), 'hex');
  const encryptedText = textParts.join(':');
  const decipher = crypto.createDecipher('aes-256-cbc', secretKey);
  let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

Environment Variables

// Use dotenv for environment variables
require('dotenv').config();

// Store secrets in .env file
// DB_PASSWORD=your_secret_password
// JWT_SECRET=your_jwt_secret
// API_KEY=your_api_key

// Access in code
const dbPassword = process.env.DB_PASSWORD;
const jwtSecret = process.env.JWT_SECRET;

Rate Limiting

Preventing Brute Force Attacks

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

// General rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per window
  message: 'Too many requests from this IP'
});

app.use(limiter);

// Stricter rate limiting for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // 5 attempts per 15 minutes
  skipSuccessfulRequests: true
});

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

Security Best Practices Checklist

Development

  • Use parameterized queries for database operations
  • Validate all user input (server-side)
  • Escape output to prevent XSS
  • Use HTTPS in production
  • Implement proper authentication
  • Add rate limiting to prevent abuse

Production

  • Set secure HTTP headers
  • Enable CORS properly
  • Use environment variables for secrets
  • Implement logging and monitoring
  • Regular security audits
  • Keep dependencies updated

Code Review

  • Check for SQL injection vulnerabilities
  • Verify XSS protection
  • Ensure proper authentication
  • Validate authorization checks
  • Review error handling (don’t leak info)

Security Testing

Using Security Linters

# Install security linter
npm install -g eslint-plugin-security

# Run security checks
eslint --plugin security src/

Dependency Security

# Check for vulnerable dependencies
npm audit

# Fix vulnerabilities
npm audit fix

Resources

Next Steps

Security is an ongoing process. Stay updated with the latest security practices and regularly audit your applications for vulnerabilities.

Last updated on