Web Security Essentials — Protecting Your Applications

Web Security Essentials — Protecting Your Applications

10/23/2025 Security By Tech Writers
SecurityWeb DevelopmentOWASPCryptographyBest PracticesBackend SecurityFrontend SecurityDevOps

Introduction: Security is Everyone’s Responsibility

Web security isn’t just a technical concern—it’s a business imperative. Data breaches cost organizations millions of dollars in recovery, legal fees, and reputation damage. More importantly, they harm users whose personal information is compromised. Every developer bears responsibility for building secure applications.

This comprehensive guide covers essential security principles, common vulnerabilities, and practical implementation strategies.

Table of Contents

Why Security Matters

  • Data Protection: Safeguard user privacy
  • Business Continuity: Prevent costly breaches
  • Compliance: Meet regulatory requirements (GDPR, HIPAA, etc.)
  • Reputation: Maintain user trust
  • Legal: Avoid liability and lawsuits

OWASP Top 10 (2021)

The Open Web Application Security Project (OWASP) identifies the most critical web security risks. Understanding these vulnerabilities is essential for building secure applications.

1. Broken Access Control

Users can act outside their intended permissions. This is the most common and critical vulnerability.

// ❌ Bad - No access control
router.get('/admin/dashboard', (req, res) => {
  res.json(sensitiveData);
});

// ✅ Good - Check authorization
router.get('/admin/dashboard', (req, res) => {
  if (req.user.role !== 'ADMIN') {
    return res.status(403).json({ error: 'Unauthorized' });
  }
  res.json(sensitiveData);
});

// Better - Use middleware
function requireRole(role) {
  return (req, res, next) => {
    if (req.user?.role !== role) {
      return res.status(403).json({ error: 'Unauthorized' });
    }
    next();
  };
}

router.get('/admin/dashboard', requireRole('ADMIN'), (req, res) => {
  res.json(sensitiveData);
});

2. Cryptographic Failures

Sensitive data exposed due to poor encryption practices. Always encrypt sensitive data in transit and at rest.

// ❌ Bad - Storing plaintext passwords
db.users.create({ email, password }); // NO!

// ✅ Good - Hash passwords with bcrypt
const bcrypt = require('bcrypt');

async function registerUser(email, password) {
  const hashedPassword = await bcrypt.hash(password, 10);
  db.users.create({ email, hashedPassword });
}

async function loginUser(email, password) {
  const user = await db.users.findOne({ email });
  const isValid = await bcrypt.compare(password, user.hashedPassword);
  
  if (!isValid) {
    throw new Error('Invalid credentials');
  }
  
  return user;
}

3. Injection

Untrusted data sent to interpreter as part of command. This includes SQL, NoSQL, OS command, and LDAP injection.

Injection Attacks

SQL Injection

// ❌ Bad - Concatenating user input
const query = `SELECT * FROM users WHERE email = '${email}'`;
db.query(query);

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

// With ORM (Prisma)
const user = await prisma.user.findUnique({
  where: { email }
});

NoSQL Injection

NoSQL databases are vulnerable to injection if queries accept raw user input. Always validate and sanitize input.

// ❌ Bad - Direct query object manipulation
const user = await User.findOne({ email: req.body.email });

// ✅ Good - Validate and sanitize input
const email = String(req.body.email).trim();
const user = await User.findOne({ email });

// Even better - Use schema validation
import { z } from 'zod';

const emailSchema = z.string().email();
const email = emailSchema.parse(req.body.email);
const user = await User.findOne({ email });

OS Command Injection

// ❌ Bad - Using shell commands with user input
const { execSync } = require('child_process');
execSync(`convert ${imagePath} thumbnail.jpg`);

// ✅ Good - Use libraries instead of shell
const ImageMagick = require('imagemagick');
ImageMagick.convert([imagePath, 'thumbnail.jpg']);

// Or use safe APIs with argument arrays
const { spawn } = require('child_process');
spawn('convert', [imagePath, 'thumbnail.jpg']);

Cross-Site Scripting (XSS)

Injecting malicious scripts into web pages viewed by other users. XSS can steal cookies, hijack sessions, or deface content.

Stored XSS Prevention

// ❌ Bad - Rendering unsanitized HTML
app.get('/posts/:id', (req, res) => {
  const post = db.posts.find(req.params.id);
  res.send(`<h1>${post.title}</h1><p>${post.content}</p>`);
});

// ✅ Good - Escape HTML
const { escapeHtml } = require('escape-html');

app.get('/posts/:id', (req, res) => {
  const post = db.posts.find(req.params.id);
  res.send(`
    <h1>${escapeHtml(post.title)}</h1>
    <p>${escapeHtml(post.content)}</p>
  `);
});

// Using template engines (auto-escaping)
// Handlebars, EJS, etc. escape by default
res.render('post', { post }); // Safe

DOM XSS Prevention

DOM-based XSS occurs when JavaScript code constructs HTML from user input. Always use safe DOM methods or sanitization libraries.

// ❌ Bad - Using innerHTML with user data
document.getElementById('content').innerHTML = userInput;

// ✅ Good - Use textContent for text
document.getElementById('content').textContent = userInput;

// Or use safe DOM methods
const element = document.createElement('p');
element.textContent = userInput;
document.body.appendChild(element);

// Use security libraries
const DOMPurify = require('dompurify');
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;

Cross-Site Request Forgery (CSRF)

Attacker tricks user into making unwanted requests on their behalf. CSRF tokens and SameSite cookies prevent this attack.

CSRF Protection

// Generate CSRF token
const csrfProtection = csrf({ cookie: true });

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

// Validate CSRF token
app.post('/transfer', csrfProtection, (req, res) => {
  // CSRF token automatically validated
  res.json({ success: true });
});

// In HTML form
<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <input type="text" name="amount">
  <button type="submit">Transfer</button>
</form>

// For AJAX requests
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': token,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ amount: 100 })
});

Authentication & Authorization

Strong authentication and proper authorization checks are foundational to application security.

Secure Password Storage

const argon2 = require('argon2');

// Hash password with Argon2 (more secure than bcrypt)
async function hashPassword(password) {
  return await argon2.hash(password);
}

async function verifyPassword(password, hash) {
  return await argon2.verify(hash, password);
}

// Password requirements
const PASSWORD_REQUIREMENTS = {
  minLength: 12,
  requireUppercase: true,
  requireLowercase: true,
  requireNumbers: true,
  requireSpecialChars: true
};

function validatePassword(password) {
  const hasUppercase = /[A-Z]/.test(password);
  const hasLowercase = /[a-z]/.test(password);
  const hasNumbers = /\d/.test(password);
  const hasSpecialChars = /[!@#$%^&*]/.test(password);
  const isLongEnough = password.length >= PASSWORD_REQUIREMENTS.minLength;
  
  return hasUppercase && hasLowercase && hasNumbers && hasSpecialChars && isLongEnough;
}

JWT Token Security

const jwt = require('jsonwebtoken');

// Generate token with expiration
function generateToken(userId) {
  return jwt.sign(
    { userId },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
}

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

// Store token in httpOnly cookie (not localStorage)
res.cookie('token', token, {
  httpOnly: true,      // Not accessible via JavaScript
  secure: true,        // HTTPS only
  sameSite: 'Strict',  // CSRF protection
  maxAge: 3600000      // 1 hour
});

Role-Based Access Control (RBAC)

const PERMISSIONS = {
  ADMIN: ['create', 'read', 'update', 'delete'],
  MODERATOR: ['read', 'update', 'delete'],
  USER: ['read']
};

function canPerform(userRole, action) {
  return PERMISSIONS[userRole]?.includes(action);
}

function authorizeAction(action) {
  return (req, res, next) => {
    if (!canPerform(req.user.role, action)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

app.post('/posts', authorizeAction('create'), createPost);
app.delete('/posts/:id', authorizeAction('delete'), deletePost);

Encryption & Hashing

Symmetric Encryption

const crypto = require('crypto');

function encryptData(plaintext, key) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  
  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  
  const authTag = cipher.getAuthTag();
  
  return {
    iv: iv.toString('hex'),
    encrypted,
    authTag: authTag.toString('hex')
  };
}

function decryptData(encrypted, key) {
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(encrypted.iv, 'hex')
  );
  
  decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
  
  let decrypted = decipher.update(encrypted.encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  
  return decrypted;
}

Secure Communication

HTTPS Only

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

const app = express();

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(`https://${req.header('host')}${req.url}`);
  } else {
    next();
  }
});

const options = {
  key: fs.readFileSync('path/to/key.pem'),
  cert: fs.readFileSync('path/to/cert.pem')
};

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

API Security

Rate Limiting

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

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // limit each IP to 100 requests per windowMs
  message: 'Too many requests, please try again later.'
});

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

// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true
});

app.post('/login', authLimiter, loginHandler);

CORS Security

const cors = require('cors');

// ❌ Bad - Allow all origins
app.use(cors());

// ✅ Good - Whitelist specific origins
const allowedOrigins = [
  'https://example.com',
  'https://app.example.com'
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
}));

Security Headers

const helmet = require('helmet');

app.use(helmet());

// Helmet sets these headers:
// Content-Security-Policy
// X-Frame-Options: DENY
// X-Content-Type-Options: nosniff
// X-XSS-Protection: 1; mode=block
// Strict-Transport-Security
// Referrer-Policy

Dependency Management

Check for Vulnerabilities

# NPM audit
npm audit

# Fix vulnerabilities
npm audit fix

# Automated dependency updates
npm update

# Check specific package
npm info package-name

Keep Dependencies Updated

// package.json
{
  "dependencies": {
    "express": "^4.18.0",  // Minor version updates
    "lodash": "~4.17.0"    // Patch updates only
  }
}

Best Practices

  1. Use HTTPS Everywhere: Never use HTTP in production
  2. Keep Dependencies Updated: Regularly update packages
  3. Principle of Least Privilege: Grant minimum necessary permissions
  4. Defense in Depth: Multiple layers of security
  5. Input Validation: Validate all user input
  6. Output Encoding: Escape all user-controlled output
  7. Security Testing: Include security in your test suite
  8. Logging & Monitoring: Track and alert on suspicious activity
  9. Secure Configuration: Keep secrets in environment variables
  10. Education: Developers must understand security

Conclusion

Web security is an ongoing process, not a one-time fix. By understanding OWASP Top 10 vulnerabilities, implementing proper authentication and authorization, using encryption correctly, and following security best practices, you can build applications that protect user data and business interests. Remember: security is everyone’s responsibility—every line of code you write either helps or hinders overall security posture.