A
Aghyad Alghazawi
1min read

Essential security practices and patterns for protecting web applications and user data.

Web Security: Essential Practices for Modern Applications

Security breaches cost companies millions and damage user trust. This guide covers essential security practices every developer needs to protect applications and users.

Table of Contents

  1. Authentication & Authorization
  2. Data Protection
  3. Input Validation
  4. API Security
  5. Common Vulnerabilities
  6. Security Headers

Authentication & Authorization

JWT Best Practices

class JWTManager {
  constructor() {
    this.accessTokenSecret = process.env.JWT_ACCESS_SECRET;
    this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET;
    this.accessTokenExpiry = "15m";
    this.refreshTokenExpiry = "7d";
  }

  generateTokens(payload) {
    const accessToken = jwt.sign(payload, this.accessTokenSecret, {
      expiresIn: this.accessTokenExpiry,
      issuer: "your-app",
      audience: "your-app-users",
    });

    const refreshToken = jwt.sign(
      { userId: payload.userId },
      this.refreshTokenSecret,
      { expiresIn: this.refreshTokenExpiry },
    );

    return { accessToken, refreshToken };
  }

  verifyAccessToken(token) {
    try {
      return jwt.verify(token, this.accessTokenSecret);
    } catch (error) {
      throw new Error("Invalid access token");
    }
  }
}

Role-Based Access Control

class RBACManager {
  constructor() {
    this.roles = new Map([
      ["viewer", ["read:posts"]],
      ["editor", ["read:posts", "write:posts"]],
      ["admin", ["read:posts", "write:posts", "delete:posts", "manage:users"]],
    ]);
  }

  hasPermission(userRole, requiredPermission) {
    const rolePermissions = this.roles.get(userRole);
    return rolePermissions?.includes(requiredPermission) || false;
  }

  requirePermission(permission) {
    return (req, res, next) => {
      const userRole = req.user?.role;

      if (!userRole || !this.hasPermission(userRole, permission)) {
        return res.status(403).json({
          error: "Insufficient permissions",
          required: permission,
        });
      }

      next();
    };
  }
}

// Usage
const rbac = new RBACManager();
app.get(
  "/admin/users",
  rbac.requirePermission("manage:users"),
  getUsersHandler,
);

Secure Password Handling

const bcrypt = require("bcrypt");
const zxcvbn = require("zxcvbn");

class PasswordManager {
  constructor() {
    this.saltRounds = 12;
    this.minPasswordScore = 3; // Strong password required
  }

  async hashPassword(password) {
    const strength = zxcvbn(password);
    if (strength.score < this.minPasswordScore) {
      throw new Error(
        `Password too weak: ${strength.feedback.suggestions.join(" ")}`,
      );
    }

    return await bcrypt.hash(password, this.saltRounds);
  }

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

Data Protection

Environment Variables & Secrets

# .env file structure
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_ACCESS_SECRET=your-jwt-access-secret-key
JWT_REFRESH_SECRET=your-jwt-refresh-secret-key
STRIPE_SECRET_KEY=sk_test_...
AES_ENCRYPTION_KEY=your-aes-256-key

Data Encryption

const crypto = require("crypto");

class EncryptionManager {
  constructor() {
    this.algorithm = "aes-256-gcm";
    this.key = Buffer.from(process.env.AES_ENCRYPTION_KEY, "hex");
  }

  encrypt(text) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher(this.algorithm, this.key);
    cipher.setAAD(Buffer.from("additional-data"));

    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"),
    };
  }

  decrypt(encryptedData) {
    const { encrypted, iv, authTag } = encryptedData;
    const decipher = crypto.createDecipher(this.algorithm, this.key);

    decipher.setAuthTag(Buffer.from(authTag, "hex"));
    decipher.setAAD(Buffer.from("additional-data"));

    let decrypted = decipher.update(encrypted, "hex", "utf8");
    decrypted += decipher.final("utf8");

    return decrypted;
  }
}

Database Security

// Use parameterized queries to prevent SQL injection
class UserRepository {
  async getUserByEmail(email) {
    // Good: Parameterized query
    const query = "SELECT * FROM users WHERE email = $1";
    return await db.query(query, [email]);
  }

  async createUser(userData) {
    const query = `
      INSERT INTO users (email, password_hash, created_at)
      VALUES ($1, $2, NOW())
      RETURNING id, email, created_at
    `;
    return await db.query(query, [userData.email, userData.passwordHash]);
  }
}

// Database connection with SSL
const dbConfig = {
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  ssl: {
    rejectUnauthorized: true,
    ca: fs.readFileSync("ca-certificate.crt").toString(),
  },
  max: 20, // Connection pool limit
  idleTimeoutMillis: 30000,
};

Input Validation

Request Validation Middleware

const Joi = require("joi");

class ValidationMiddleware {
  static validateBody(schema) {
    return (req, res, next) => {
      const { error, value } = schema.validate(req.body);

      if (error) {
        return res.status(400).json({
          error: "Validation failed",
          details: error.details.map((d) => d.message),
        });
      }

      req.body = value; // Use sanitized value
      next();
    };
  }

  static validateQuery(schema) {
    return (req, res, next) => {
      const { error, value } = schema.validate(req.query);

      if (error) {
        return res.status(400).json({
          error: "Invalid query parameters",
          details: error.details.map((d) => d.message),
        });
      }

      req.query = value;
      next();
    };
  }
}

// Usage schemas
const userCreateSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
  name: Joi.string().min(2).max(50).required(),
});

const userQuerySchema = Joi.object({
  page: Joi.number().integer().min(1).default(1),
  limit: Joi.number().integer().min(1).max(100).default(10),
  search: Joi.string().max(100).optional(),
});

// Route with validation
app.post(
  "/users",
  ValidationMiddleware.validateBody(userCreateSchema),
  createUser,
);
app.get(
  "/users",
  ValidationMiddleware.validateQuery(userQuerySchema),
  getUsers,
);

XSS Prevention

const DOMPurify = require("isomorphic-dompurify");

class XSSProtection {
  static sanitizeHTML(dirty) {
    return DOMPurify.sanitize(dirty, {
      ALLOWED_TAGS: ["b", "i", "em", "strong", "p", "br"],
      ALLOWED_ATTR: [],
    });
  }

  static escapeHTML(text) {
    const map = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#x27;",
    };

    return text.replace(/[&<>"']/g, (m) => map[m]);
  }

  static middleware() {
    return (req, res, next) => {
      // Sanitize request body
      if (req.body && typeof req.body === "object") {
        req.body = this.sanitizeObject(req.body);
      }
      next();
    };
  }

  static sanitizeObject(obj) {
    const sanitized = {};
    for (const [key, value] of Object.entries(obj)) {
      if (typeof value === "string") {
        sanitized[key] = this.escapeHTML(value);
      } else if (typeof value === "object" && value !== null) {
        sanitized[key] = this.sanitizeObject(value);
      } else {
        sanitized[key] = value;
      }
    }
    return sanitized;
  }
}

API Security

Rate Limiting

const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");
const Redis = require("redis");

const redisClient = Redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
});

// Different rate limits for different endpoints
const createRateLimiter = (windowMs, max, message) => {
  return rateLimit({
    store: new RedisStore({
      client: redisClient,
      prefix: "rl:",
    }),
    windowMs,
    max,
    message: { error: message },
    standardHeaders: true,
    legacyHeaders: false,
  });
};

// Apply rate limiting
app.use(
  "/api/auth/login",
  createRateLimiter(15 * 60 * 1000, 5, "Too many login attempts"),
);
app.use(
  "/api/auth/register",
  createRateLimiter(60 * 60 * 1000, 3, "Too many registration attempts"),
);
app.use("/api/", createRateLimiter(15 * 60 * 1000, 100, "Too many requests"));

API Key Management

class APIKeyManager {
  constructor() {
    this.keys = new Map(); // In production, use database
  }

  generateAPIKey(userId, permissions = []) {
    const key = `ak_${crypto.randomBytes(32).toString("hex")}`;

    this.keys.set(key, {
      userId,
      permissions,
      createdAt: new Date(),
      lastUsed: null,
      isActive: true,
    });

    return key;
  }

  validateAPIKey(key) {
    const keyData = this.keys.get(key);

    if (!keyData || !keyData.isActive) {
      throw new Error("Invalid API key");
    }

    keyData.lastUsed = new Date();
    return keyData;
  }

  middleware() {
    return (req, res, next) => {
      const apiKey = req.headers["x-api-key"];

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

      try {
        const keyData = this.validateAPIKey(apiKey);
        req.apiKey = keyData;
        next();
      } catch (error) {
        return res.status(401).json({ error: error.message });
      }
    };
  }
}

CORS Configuration

const cors = require("cors");

const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      "https://yourdomain.com",
      "https://app.yourdomain.com",
    ];

    // Allow requests with no origin (mobile apps, etc.)
    if (!origin) return callback(null, true);

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Not allowed by CORS"));
    }
  },
  credentials: true,
  optionsSuccessStatus: 200,
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization", "X-API-Key"],
};

app.use(cors(corsOptions));

Common Vulnerabilities

SQL Injection Prevention

// Bad: String concatenation
const badQuery = `SELECT * FROM users WHERE email = '${email}'`;

// Good: Parameterized queries
const goodQuery = "SELECT * FROM users WHERE email = $1";
const result = await db.query(goodQuery, [email]);

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

CSRF Protection

const csrf = require("csurf");

// CSRF protection middleware
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
  },
});

app.use(csrfProtection);

// Provide CSRF token to frontend
app.get("/api/csrf-token", (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

File Upload Security

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

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(
      null,
      file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname),
    );
  },
});

const fileFilter = (req, file, cb) => {
  const allowedTypes = ["image/jpeg", "image/png", "image/gif"];

  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error("Invalid file type"), false);
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB limit
    files: 1,
  },
});

app.post("/upload", upload.single("image"), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: "No file uploaded" });
  }

  res.json({ filename: req.file.filename });
});

Security Headers

Essential Security Headers

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"],
        imgSrc: ["'self'", "data:", "https:"],
        scriptSrc: ["'self'"],
        connectSrc: ["'self'", "https://api.yourdomain.com"],
      },
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true,
    },
  }),
);

// Additional security headers
app.use((req, res, next) => {
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("X-Frame-Options", "DENY");
  res.setHeader("X-XSS-Protection", "1; mode=block");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  next();
});

Content Security Policy

const cspDirectives = {
  defaultSrc: ["'self'"],
  scriptSrc: [
    "'self'",
    "'unsafe-inline'", // Avoid if possible
    "https://cdn.jsdelivr.net",
  ],
  styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
  fontSrc: ["'self'", "https://fonts.gstatic.com"],
  imgSrc: ["'self'", "data:", "https:"],
  connectSrc: ["'self'", "https://api.yourdomain.com"],
  frameSrc: ["'none'"],
  objectSrc: ["'none'"],
  upgradeInsecureRequests: [],
};

app.use(
  helmet.contentSecurityPolicy({
    directives: cspDirectives,
  }),
);

Security Monitoring

Audit Logging

class SecurityAuditLogger {
  constructor() {
    this.logger = require("winston").createLogger({
      level: "info",
      format: winston.format.json(),
      transports: [
        new winston.transports.File({ filename: "security-audit.log" }),
      ],
    });
  }

  logAuthAttempt(email, success, ip, userAgent) {
    this.logger.info("auth_attempt", {
      email,
      success,
      ip,
      userAgent,
      timestamp: new Date().toISOString(),
    });
  }

  logPermissionDenied(userId, resource, action, ip) {
    this.logger.warn("permission_denied", {
      userId,
      resource,
      action,
      ip,
      timestamp: new Date().toISOString(),
    });
  }

  logSuspiciousActivity(type, details, ip) {
    this.logger.error("suspicious_activity", {
      type,
      details,
      ip,
      timestamp: new Date().toISOString(),
    });
  }
}

Intrusion Detection

class IntrusionDetection {
  constructor() {
    this.suspiciousPatterns = [
      /(\bor\b|\band\b).*=.*\d/i, // SQL injection patterns
      /<script[^>]*>.*?<\/script>/i, // XSS patterns
      /\.\.\//g, // Path traversal
    ];
    this.failedAttempts = new Map();
  }

  checkRequest(req) {
    const suspicious = [];

    // Check for suspicious patterns in query params
    for (const [key, value] of Object.entries(req.query)) {
      if (this.containsSuspiciousPattern(value)) {
        suspicious.push(`Suspicious query param: ${key}`);
      }
    }

    // Check request body
    if (req.body && typeof req.body === "string") {
      if (this.containsSuspiciousPattern(req.body)) {
        suspicious.push("Suspicious request body");
      }
    }

    return suspicious;
  }

  containsSuspiciousPattern(input) {
    return this.suspiciousPatterns.some((pattern) => pattern.test(input));
  }

  trackFailedAttempt(ip) {
    const attempts = this.failedAttempts.get(ip) || 0;
    this.failedAttempts.set(ip, attempts + 1);

    if (attempts > 5) {
      return { blocked: true, reason: "Too many failed attempts" };
    }

    return { blocked: false };
  }
}

Best Practices

Security Checklist

  • Authentication: Use strong password policies, implement MFA, secure session management
  • Authorization: Implement RBAC, validate permissions on every request
  • Data Protection: Encrypt sensitive data, use HTTPS everywhere, secure database connections
  • Input Validation: Validate and sanitize all inputs, use parameterized queries
  • Error Handling: Don't expose sensitive information in error messages
  • Logging: Log security events, monitor for suspicious activity
  • Updates: Keep dependencies updated, regularly scan for vulnerabilities

Development Guidelines

// Security middleware stack
app.use(helmet()); // Security headers
app.use(cors(corsOptions)); // CORS configuration
app.use(rateLimit(rateLimitOptions)); // Rate limiting
app.use(XSSProtection.middleware()); // XSS protection
app.use(csrfProtection); // CSRF protection

// Authentication middleware
app.use("/api/protected", authenticateToken);

// Authorization middleware
app.use("/api/admin", requireRole("admin"));

Environment Security

  • Use environment variables for secrets
  • Implement proper secret rotation
  • Use secure hosting configurations
  • Regular security audits and penetration testing
  • Implement proper backup and disaster recovery

Conclusion

Security is not a one-time implementation but an ongoing process. Implement these practices as part of your development workflow, regularly audit your applications, and stay updated with the latest security threats and best practices.

Remember: security is only as strong as its weakest link. A comprehensive approach covering all layers of your application is essential for protecting your users and business.