Security - RumenDamyanov/js-chess GitHub Wiki

Security Considerations

Comprehensive security guide for the chess showcase application covering frontend, backend, and infrastructure security.

Overview

Security considerations for this chess application include:

  • Frontend security (XSS, CSRF, data validation)
  • API security and authentication
  • WebSocket connection security
  • Data protection and privacy
  • Infrastructure and deployment security
  • Real-time communication security
  • User input validation and sanitization

Frontend Security

Cross-Site Scripting (XSS) Prevention

Input Sanitization

// shared/security/InputSanitizer.js
export class InputSanitizer {
  constructor() {
    this.htmlEntities = {
      '&': '&',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      '/': '&#x2F;'
    };
  }

  sanitizeHtml(input) {
    if (typeof input !== 'string') return '';

    return input.replace(/[&<>"'/]/g, (char) => {
      return this.htmlEntities[char] || char;
    });
  }

  sanitizeChessMove(moveNotation) {
    // Chess moves should only contain specific characters
    const validChars = /^[a-h1-8NBRQK+#=\-x0O]+$/;

    if (!validChars.test(moveNotation)) {
      throw new Error('Invalid chess notation');
    }

    return moveNotation.substring(0, 10); // Limit length
  }

  sanitizeUsername(username) {
    // Remove potentially harmful characters
    const cleaned = username
      .replace(/[<>"\\/]/g, '')
      .substring(0, 50)
      .trim();

    if (cleaned.length < 2) {
      throw new Error('Username too short');
    }

    return cleaned;
  }

  sanitizeChatMessage(message) {
    if (typeof message !== 'string') return '';

    // Remove HTML tags and limit length
    const cleaned = message
      .replace(/<[^>]*>/g, '')
      .substring(0, 500)
      .trim();

    return this.sanitizeHtml(cleaned);
  }

  validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    if (!emailRegex.test(email) || email.length > 254) {
      throw new Error('Invalid email format');
    }

    return email.toLowerCase();
  }
}

export const inputSanitizer = new InputSanitizer();

Content Security Policy (CSP)

<!-- CSP implementation in HTML -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https:;
  connect-src 'self' ws: wss:;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
">
// CSP configuration for Express.js backend
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "ws:", "wss:"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"]
    }
  },
  crossOriginEmbedderPolicy: false // Disable for WebSocket support
}));

Framework-Specific XSS Prevention

Angular:

// Angular automatic XSS protection
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Component({
  selector: 'app-chat-message',
  template: `
    <!-- Angular automatically sanitizes this -->
    <div class="message">{{ message.text }}</div>

    <!-- For trusted HTML (use sparingly) -->
    <div [innerHTML]="trustedHtml"></div>
  `
})
export class ChatMessageComponent {
  message: any;
  trustedHtml: SafeHtml;

  constructor(private sanitizer: DomSanitizer) {}

  setTrustedContent(html: string) {
    // Only use for content you absolutely trust
    this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(html);
  }
}

React:

// React XSS prevention
import DOMPurify from 'dompurify';

function ChatMessage({ message }) {
  // React automatically escapes content in JSX
  return (
    <div className="message">
      {message.text}
    </div>
  );
}

function TrustedHtmlMessage({ htmlContent }) {
  // For HTML content that needs to be rendered
  const sanitizedHtml = DOMPurify.sanitize(htmlContent);

  return (
    <div
      dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
    />
  );
}

Vue:

<template>
  <div class="message">
    <!-- Vue automatically escapes this -->
    {{ message.text }}

    <!-- For trusted HTML -->
    <div v-html="sanitizedHtml"></div>
  </div>
</template>

<script setup>
import DOMPurify from 'dompurify';
import { computed } from 'vue';

const props = defineProps(['message']);

const sanitizedHtml = computed(() => {
  return DOMPurify.sanitize(props.message.html || '');
});
</script>

Cross-Site Request Forgery (CSRF) Prevention

// shared/security/CsrfProtection.js
export class CsrfProtection {
  constructor() {
    this.token = this.generateToken();
    this.tokenName = 'X-CSRF-Token';
  }

  generateToken() {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
  }

  getToken() {
    return this.token;
  }

  addTokenToRequest(request) {
    if (request.method !== 'GET') {
      request.headers = request.headers || {};
      request.headers[this.tokenName] = this.token;
    }
    return request;
  }

  validateToken(receivedToken) {
    return this.token === receivedToken;
  }

  refreshToken() {
    this.token = this.generateToken();
    return this.token;
  }
}

// Usage in API client
export class SecureApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.csrfProtection = new CsrfProtection();
  }

  async request(method, endpoint, data = null) {
    const config = {
      method,
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': this.csrfProtection.getToken()
      },
      credentials: 'same-origin' // Include cookies
    };

    if (data) {
      config.body = JSON.stringify(data);
    }

    const response = await fetch(this.baseURL + endpoint, config);

    // Handle CSRF token refresh
    if (response.status === 403) {
      const newToken = response.headers.get('X-CSRF-Token');
      if (newToken) {
        this.csrfProtection.token = newToken;
        // Retry request with new token
        config.headers['X-CSRF-Token'] = newToken;
        return fetch(this.baseURL + endpoint, config);
      }
    }

    return response;
  }
}

Authentication and Authorization

JWT Token Management

// shared/auth/TokenManager.js
export class TokenManager {
  constructor() {
    this.accessToken = null;
    this.refreshToken = null;
    this.tokenExpiry = null;
    this.refreshThreshold = 5 * 60 * 1000; // 5 minutes before expiry
  }

  setTokens(accessToken, refreshToken) {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;

    // Decode token to get expiry
    try {
      const payload = JSON.parse(atob(accessToken.split('.')[1]));
      this.tokenExpiry = payload.exp * 1000; // Convert to milliseconds
    } catch (error) {
      console.error('Invalid token format:', error);
    }

    this.saveToStorage();
  }

  getAccessToken() {
    if (this.isTokenExpired()) {
      return null;
    }

    return this.accessToken;
  }

  isTokenExpired() {
    if (!this.tokenExpiry) return true;
    return Date.now() >= this.tokenExpiry;
  }

  shouldRefresh() {
    if (!this.tokenExpiry) return false;
    return Date.now() >= (this.tokenExpiry - this.refreshThreshold);
  }

  async refreshAccessToken() {
    if (!this.refreshToken) {
      throw new Error('No refresh token available');
    }

    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ refreshToken: this.refreshToken })
      });

      if (!response.ok) {
        throw new Error('Token refresh failed');
      }

      const { accessToken, refreshToken } = await response.json();
      this.setTokens(accessToken, refreshToken);

      return accessToken;
    } catch (error) {
      this.clearTokens();
      throw error;
    }
  }

  saveToStorage() {
    try {
      const tokenData = {
        accessToken: this.accessToken,
        refreshToken: this.refreshToken,
        tokenExpiry: this.tokenExpiry
      };

      // Use sessionStorage for temporary tokens or localStorage for persistent
      localStorage.setItem('auth-tokens', JSON.stringify(tokenData));
    } catch (error) {
      console.error('Failed to save tokens:', error);
    }
  }

  loadFromStorage() {
    try {
      const stored = localStorage.getItem('auth-tokens');
      if (stored) {
        const tokenData = JSON.parse(stored);
        this.accessToken = tokenData.accessToken;
        this.refreshToken = tokenData.refreshToken;
        this.tokenExpiry = tokenData.tokenExpiry;
      }
    } catch (error) {
      console.error('Failed to load tokens:', error);
      this.clearTokens();
    }
  }

  clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;
    this.tokenExpiry = null;

    try {
      localStorage.removeItem('auth-tokens');
    } catch (error) {
      console.error('Failed to clear tokens:', error);
    }
  }
}

export const tokenManager = new TokenManager();

Secure API Interceptors

// shared/auth/AuthInterceptor.js
export class AuthInterceptor {
  constructor(tokenManager, apiClient) {
    this.tokenManager = tokenManager;
    this.apiClient = apiClient;
    this.isRefreshing = false;
    this.refreshPromise = null;
  }

  async interceptRequest(request) {
    // Check if token needs refresh
    if (this.tokenManager.shouldRefresh() && !this.isRefreshing) {
      await this.refreshToken();
    }

    // Add authorization header
    const token = this.tokenManager.getAccessToken();
    if (token) {
      request.headers = request.headers || {};
      request.headers['Authorization'] = `Bearer ${token}`;
    }

    return request;
  }

  async interceptResponse(response, request) {
    // Handle 401 unauthorized
    if (response.status === 401) {
      try {
        await this.refreshToken();

        // Retry original request with new token
        const newToken = this.tokenManager.getAccessToken();
        if (newToken) {
          request.headers['Authorization'] = `Bearer ${newToken}`;
          return fetch(request.url, request);
        }
      } catch (error) {
        // Refresh failed, redirect to login
        this.handleAuthenticationFailure();
        throw error;
      }
    }

    return response;
  }

  async refreshToken() {
    if (this.isRefreshing) {
      return this.refreshPromise;
    }

    this.isRefreshing = true;
    this.refreshPromise = this.tokenManager.refreshAccessToken()
      .finally(() => {
        this.isRefreshing = false;
        this.refreshPromise = null;
      });

    return this.refreshPromise;
  }

  handleAuthenticationFailure() {
    this.tokenManager.clearTokens();

    // Emit authentication failure event
    const event = new CustomEvent('authenticationFailure');
    document.dispatchEvent(event);

    // Redirect to login (adjust based on your routing)
    if (typeof window !== 'undefined') {
      window.location.href = '/login';
    }
  }
}

WebSocket Security

Secure WebSocket Implementation

// shared/websocket/SecureWebSocket.js
export class SecureWebSocket {
  constructor(url, tokenManager, options = {}) {
    this.url = url;
    this.tokenManager = tokenManager;
    this.options = {
      maxMessageSize: 64 * 1024, // 64KB max message size
      heartbeatInterval: 30000,
      authTimeout: 5000,
      ...options
    };

    this.ws = null;
    this.isAuthenticated = false;
    this.messageQueue = [];
    this.heartbeatTimer = null;
    this.rateLimiter = new RateLimiter(100, 60000); // 100 messages per minute
  }

  async connect() {
    const token = this.tokenManager.getAccessToken();
    if (!token) {
      throw new Error('No authentication token available');
    }

    // Use secure WebSocket protocol
    const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
    const wsUrl = `${protocol}//${location.host}${this.url}`;

    this.ws = new WebSocket(wsUrl);

    return new Promise((resolve, reject) => {
      const authTimeout = setTimeout(() => {
        this.ws.close();
        reject(new Error('WebSocket authentication timeout'));
      }, this.options.authTimeout);

      this.ws.onopen = () => {
        // Send authentication token
        this.send({
          type: 'auth',
          token: token
        });
      };

      this.ws.onmessage = (event) => {
        const message = this.parseMessage(event.data);

        if (message.type === 'auth_success') {
          clearTimeout(authTimeout);
          this.isAuthenticated = true;
          this.startHeartbeat();
          this.flushMessageQueue();
          resolve();
        } else if (message.type === 'auth_failed') {
          clearTimeout(authTimeout);
          this.ws.close();
          reject(new Error('WebSocket authentication failed'));
        } else if (this.isAuthenticated) {
          this.handleMessage(message);
        }
      };

      this.ws.onclose = () => {
        clearTimeout(authTimeout);
        this.stopHeartbeat();
        this.isAuthenticated = false;
      };

      this.ws.onerror = (error) => {
        clearTimeout(authTimeout);
        reject(error);
      };
    });
  }

  send(message) {
    // Rate limiting
    if (!this.rateLimiter.allow()) {
      throw new Error('Rate limit exceeded');
    }

    // Message size validation
    const messageStr = JSON.stringify(message);
    if (messageStr.length > this.options.maxMessageSize) {
      throw new Error('Message too large');
    }

    if (!this.isAuthenticated) {
      this.messageQueue.push(message);
      return;
    }

    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(messageStr);
    }
  }

  parseMessage(data) {
    try {
      const message = JSON.parse(data);

      // Validate message structure
      if (typeof message !== 'object' || !message.type) {
        throw new Error('Invalid message format');
      }

      return message;
    } catch (error) {
      console.error('Failed to parse WebSocket message:', error);
      return null;
    }
  }

  handleMessage(message) {
    // Validate message types
    const allowedTypes = [
      'game_update', 'move', 'chat', 'heartbeat_response',
      'player_joined', 'player_left', 'game_ended'
    ];

    if (!allowedTypes.includes(message.type)) {
      console.warn('Unknown message type:', message.type);
      return;
    }

    // Emit message to handlers
    this.emit('message', message);
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.send({ type: 'heartbeat' });
      }
    }, this.options.heartbeatInterval);
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.send(message);
    }
  }

  close() {
    this.stopHeartbeat();
    if (this.ws) {
      this.ws.close();
    }
  }
}

// Rate limiter utility
class RateLimiter {
  constructor(maxRequests, windowMs) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.requests = [];
  }

  allow() {
    const now = Date.now();

    // Remove old requests outside the window
    this.requests = this.requests.filter(time => now - time < this.windowMs);

    if (this.requests.length >= this.maxRequests) {
      return false;
    }

    this.requests.push(now);
    return true;
  }
}

Data Validation and Sanitization

Chess Move Validation

// shared/security/ChessMoveValidator.js
export class ChessMoveValidator {
  constructor() {
    this.pieceTypes = ['p', 'n', 'b', 'r', 'q', 'k'];
    this.files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
    this.ranks = ['1', '2', '3', '4', '5', '6', '7', '8'];
  }

  validateMove(move, gameState) {
    // Validate move object structure
    if (!this.isValidMoveObject(move)) {
      throw new SecurityError('Invalid move object structure');
    }

    // Validate move notation
    if (!this.isValidNotation(move.notation)) {
      throw new SecurityError('Invalid move notation');
    }

    // Validate board positions
    if (!this.isValidSquare(move.from) || !this.isValidSquare(move.to)) {
      throw new SecurityError('Invalid board positions');
    }

    // Validate against game rules
    if (!this.isLegalMove(move, gameState)) {
      throw new SecurityError('Illegal chess move');
    }

    return true;
  }

  isValidMoveObject(move) {
    return (
      typeof move === 'object' &&
      typeof move.from === 'string' &&
      typeof move.to === 'string' &&
      typeof move.notation === 'string' &&
      (move.promotion === undefined || this.pieceTypes.includes(move.promotion))
    );
  }

  isValidNotation(notation) {
    // Standard algebraic notation patterns
    const patterns = [
      /^[NBRQK]?[a-h]?[1-8]?x?[a-h][1-8](\=[NBRQ])?[\+\#]?$/, // Normal moves
      /^O-O(-O)?[\+\#]?$/, // Castling
      /^[a-h]x[a-h][1-8](\s*e\.p\.)?[\+\#]?$/ // En passant
    ];

    return patterns.some(pattern => pattern.test(notation));
  }

  isValidSquare(square) {
    if (typeof square !== 'string' || square.length !== 2) {
      return false;
    }

    const [file, rank] = square;
    return this.files.includes(file) && this.ranks.includes(rank);
  }

  isLegalMove(move, gameState) {
    // This should integrate with your chess engine
    // for full legal move validation
    try {
      const chesEngine = new ChessEngine(gameState);
      return chesEngine.isLegalMove(move);
    } catch (error) {
      return false;
    }
  }
}

class SecurityError extends Error {
  constructor(message) {
    super(message);
    this.name = 'SecurityError';
  }
}

export const chessMoveValidator = new ChessMoveValidator();

User Input Validation

// shared/security/InputValidator.js
export class InputValidator {
  static validateGameId(gameId) {
    // Game IDs should be UUIDs
    const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

    if (!uuidPattern.test(gameId)) {
      throw new ValidationError('Invalid game ID format');
    }

    return gameId;
  }

  static validateUserId(userId) {
    if (typeof userId !== 'string' || userId.length < 1 || userId.length > 50) {
      throw new ValidationError('Invalid user ID');
    }

    // Only allow alphanumeric characters and hyphens
    if (!/^[a-zA-Z0-9\-]+$/.test(userId)) {
      throw new ValidationError('Invalid user ID characters');
    }

    return userId;
  }

  static validateTimeControl(timeControl) {
    if (typeof timeControl !== 'object') {
      throw new ValidationError('Time control must be an object');
    }

    const { initial, increment } = timeControl;

    if (typeof initial !== 'number' || initial < 0 || initial > 7200000) { // Max 2 hours
      throw new ValidationError('Invalid initial time');
    }

    if (typeof increment !== 'number' || increment < 0 || increment > 300000) { // Max 5 minutes
      throw new ValidationError('Invalid increment time');
    }

    return timeControl;
  }

  static validateGameSettings(settings) {
    const allowedSettings = [
      'timeControl', 'rated', 'color', 'aiLevel', 'variant'
    ];

    const validatedSettings = {};

    for (const [key, value] of Object.entries(settings)) {
      if (!allowedSettings.includes(key)) {
        throw new ValidationError(`Unknown setting: ${key}`);
      }

      switch (key) {
        case 'timeControl':
          validatedSettings[key] = this.validateTimeControl(value);
          break;
        case 'rated':
          if (typeof value !== 'boolean') {
            throw new ValidationError('Rated setting must be boolean');
          }
          validatedSettings[key] = value;
          break;
        case 'color':
          if (!['white', 'black', 'random'].includes(value)) {
            throw new ValidationError('Invalid color preference');
          }
          validatedSettings[key] = value;
          break;
        case 'aiLevel':
          if (typeof value !== 'number' || value < 1 || value > 10) {
            throw new ValidationError('AI level must be between 1 and 10');
          }
          validatedSettings[key] = value;
          break;
        case 'variant':
          if (!['standard', 'chess960', 'kingofthehill'].includes(value)) {
            throw new ValidationError('Invalid chess variant');
          }
          validatedSettings[key] = value;
          break;
      }
    }

    return validatedSettings;
  }
}

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

Secure Communication

HTTPS Enforcement

// shared/security/HttpsEnforcer.js
export class HttpsEnforcer {
  static enforceHttps() {
    if (typeof window !== 'undefined' &&
        location.protocol !== 'https:' &&
        location.hostname !== 'localhost' &&
        !location.hostname.startsWith('127.')) {

      // Redirect to HTTPS
      location.replace(`https:${location.href.substring(location.protocol.length)}`);
    }
  }

  static addSecurityHeaders() {
    // This should be implemented on the server side
    return {
      'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
      'X-Content-Type-Options': 'nosniff',
      'X-Frame-Options': 'DENY',
      'X-XSS-Protection': '1; mode=block',
      'Referrer-Policy': 'strict-origin-when-cross-origin'
    };
  }
}

// Auto-enforce HTTPS on load
if (typeof window !== 'undefined') {
  HttpsEnforcer.enforceHttps();
}

API Security Headers

// Express.js security middleware
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');

const app = express();

// Security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "wss:"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"],
    },
  },
  crossOriginEmbedderPolicy: false,
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// CORS configuration
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true,
  optionsSuccessStatus: 200
}));

// Rate limiting
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP',
  standardHeaders: true,
  legacyHeaders: false
});

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

// Stricter rate limiting for authentication endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // limit each IP to 5 requests per windowMs
  skipSuccessfulRequests: true
});

app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);

Environment and Configuration Security

Secure Environment Variables

// shared/config/SecurityConfig.js
export class SecurityConfig {
  constructor() {
    this.requiredEnvVars = [
      'JWT_SECRET',
      'DATABASE_URL',
      'REDIS_URL',
      'API_BASE_URL'
    ];

    this.validate();
  }

  validate() {
    const missing = this.requiredEnvVars.filter(
      varName => !process.env[varName]
    );

    if (missing.length > 0) {
      throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
    }

    // Validate JWT secret strength
    if (process.env.JWT_SECRET.length < 32) {
      throw new Error('JWT_SECRET must be at least 32 characters long');
    }
  }

  getClientConfig() {
    // Only expose safe configuration to the client
    return {
      apiBaseUrl: process.env.API_BASE_URL,
      wsUrl: process.env.WS_URL,
      environment: process.env.NODE_ENV,
      version: process.env.APP_VERSION
    };
  }

  getServerConfig() {
    return {
      jwtSecret: process.env.JWT_SECRET,
      databaseUrl: process.env.DATABASE_URL,
      redisUrl: process.env.REDIS_URL,
      port: parseInt(process.env.PORT || '3000', 10)
    };
  }
}

export const securityConfig = new SecurityConfig();

Secret Management

# docker-compose.yml with secrets
version: '3.8'
services:
  app:
    build: .
    secrets:
      - jwt_secret
      - db_password
    environment:
      - JWT_SECRET_FILE=/run/secrets/jwt_secret
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  jwt_secret:
    file: ./secrets/jwt_secret.txt
  db_password:
    file: ./secrets/db_password.txt
// Utility to read secrets from files
import fs from 'fs';

export function readSecret(envVar, fallback = null) {
  const secretFile = process.env[`${envVar}_FILE`];

  if (secretFile && fs.existsSync(secretFile)) {
    return fs.readFileSync(secretFile, 'utf8').trim();
  }

  return process.env[envVar] || fallback;
}

// Usage
const jwtSecret = readSecret('JWT_SECRET');
const dbPassword = readSecret('DB_PASSWORD');

Security Monitoring and Logging

Security Event Logging

// shared/security/SecurityLogger.js
export class SecurityLogger {
  constructor() {
    this.events = new Map();
    this.alertThresholds = {
      failed_login: 5,
      invalid_token: 10,
      rate_limit_exceeded: 3,
      suspicious_move: 10
    };
  }

  logSecurityEvent(type, details, userId = null, ipAddress = null) {
    const event = {
      type,
      details,
      userId,
      ipAddress,
      timestamp: new Date().toISOString(),
      userAgent: typeof window !== 'undefined' ? navigator.userAgent : null
    };

    console.warn('Security Event:', event);

    // Track event frequency
    this.trackEventFrequency(type, userId || ipAddress);

    // Send to monitoring service
    this.sendToMonitoring(event);
  }

  trackEventFrequency(type, identifier) {
    if (!identifier) return;

    const key = `${type}:${identifier}`;
    const currentCount = this.events.get(key) || 0;
    const newCount = currentCount + 1;

    this.events.set(key, newCount);

    // Check if threshold exceeded
    const threshold = this.alertThresholds[type];
    if (threshold && newCount >= threshold) {
      this.triggerAlert(type, identifier, newCount);
    }

    // Clean up old events (implement TTL)
    this.cleanupOldEvents();
  }

  triggerAlert(type, identifier, count) {
    const alert = {
      level: 'HIGH',
      type: 'SECURITY_THRESHOLD_EXCEEDED',
      eventType: type,
      identifier,
      count,
      timestamp: new Date().toISOString()
    };

    console.error('Security Alert:', alert);

    // Send alert to monitoring system
    this.sendAlert(alert);
  }

  sendToMonitoring(event) {
    // Send to external monitoring service
    if (typeof fetch !== 'undefined') {
      fetch('/api/security/events', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event)
      }).catch(error => {
        console.error('Failed to send security event:', error);
      });
    }
  }

  sendAlert(alert) {
    // Send to alerting system (email, Slack, etc.)
    if (typeof fetch !== 'undefined') {
      fetch('/api/security/alerts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(alert)
      }).catch(error => {
        console.error('Failed to send security alert:', error);
      });
    }
  }

  cleanupOldEvents() {
    // Simple cleanup - in production, implement proper TTL
    if (this.events.size > 1000) {
      const entries = Array.from(this.events.entries());
      entries.slice(0, 500).forEach(([key]) => {
        this.events.delete(key);
      });
    }
  }
}

export const securityLogger = new SecurityLogger();

Intrusion Detection

// shared/security/IntrusionDetection.js
export class IntrusionDetection {
  constructor() {
    this.suspiciousPatterns = {
      rapidMoves: { threshold: 10, timeWindow: 60000 }, // 10 moves in 1 minute
      impossibleMoves: { threshold: 3, timeWindow: 300000 }, // 3 invalid moves in 5 minutes
      tokenAbuse: { threshold: 5, timeWindow: 60000 }, // 5 token errors in 1 minute
      apiAbuse: { threshold: 100, timeWindow: 60000 } // 100 API calls in 1 minute
    };

    this.userActivity = new Map();
  }

  checkForSuspiciousActivity(userId, activityType, metadata = {}) {
    const activity = this.getUserActivity(userId);
    const now = Date.now();

    // Add new activity
    activity.push({
      type: activityType,
      timestamp: now,
      metadata
    });

    // Clean old activities
    this.cleanOldActivities(activity, now);

    // Check for patterns
    return this.analyzeActivity(userId, activity, activityType);
  }

  getUserActivity(userId) {
    if (!this.userActivity.has(userId)) {
      this.userActivity.set(userId, []);
    }
    return this.userActivity.get(userId);
  }

  cleanOldActivities(activities, currentTime) {
    const maxAge = Math.max(...Object.values(this.suspiciousPatterns).map(p => p.timeWindow));

    while (activities.length > 0 && currentTime - activities[0].timestamp > maxAge) {
      activities.shift();
    }
  }

  analyzeActivity(userId, activities, currentActivityType) {
    const threats = [];

    for (const [patternName, pattern] of Object.entries(this.suspiciousPatterns)) {
      const relevantActivities = this.getRelevantActivities(activities, patternName, pattern.timeWindow);

      if (relevantActivities.length >= pattern.threshold) {
        threats.push({
          type: patternName,
          count: relevantActivities.length,
          threshold: pattern.threshold,
          timeWindow: pattern.timeWindow,
          severity: this.calculateSeverity(relevantActivities.length, pattern.threshold)
        });
      }
    }

    if (threats.length > 0) {
      this.handleThreats(userId, threats, currentActivityType);
    }

    return threats;
  }

  getRelevantActivities(activities, patternName, timeWindow) {
    const now = Date.now();
    const cutoff = now - timeWindow;

    return activities.filter(activity => {
      if (activity.timestamp < cutoff) return false;

      switch (patternName) {
        case 'rapidMoves':
          return activity.type === 'move';
        case 'impossibleMoves':
          return activity.type === 'invalid_move';
        case 'tokenAbuse':
          return activity.type === 'token_error';
        case 'apiAbuse':
          return activity.type === 'api_call';
        default:
          return false;
      }
    });
  }

  calculateSeverity(count, threshold) {
    const ratio = count / threshold;

    if (ratio >= 3) return 'CRITICAL';
    if (ratio >= 2) return 'HIGH';
    if (ratio >= 1.5) return 'MEDIUM';
    return 'LOW';
  }

  handleThreats(userId, threats, activityType) {
    const maxSeverity = threats.reduce((max, threat) => {
      const severityOrder = { 'LOW': 1, 'MEDIUM': 2, 'HIGH': 3, 'CRITICAL': 4 };
      return severityOrder[threat.severity] > severityOrder[max] ? threat.severity : max;
    }, 'LOW');

    const threatDetails = {
      userId,
      threats,
      maxSeverity,
      timestamp: new Date().toISOString(),
      triggerActivity: activityType
    };

    securityLogger.logSecurityEvent('intrusion_detected', threatDetails, userId);

    // Take action based on severity
    this.takeAction(userId, maxSeverity, threatDetails);
  }

  takeAction(userId, severity, threatDetails) {
    switch (severity) {
      case 'CRITICAL':
        this.blockUser(userId, 'Automatic block due to critical security threat');
        break;
      case 'HIGH':
        this.temporaryRestriction(userId, 300000); // 5 minutes
        break;
      case 'MEDIUM':
        this.increaseMonitoring(userId);
        break;
      case 'LOW':
        // Just log, no action
        break;
    }
  }

  blockUser(userId, reason) {
    // Implement user blocking logic
    console.error(`Blocking user ${userId}: ${reason}`);

    // Send to backend to block user
    if (typeof fetch !== 'undefined') {
      fetch('/api/security/block-user', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, reason })
      });
    }
  }

  temporaryRestriction(userId, duration) {
    console.warn(`Temporary restriction for user ${userId} for ${duration}ms`);

    // Implement temporary restriction logic
    setTimeout(() => {
      console.log(`Lifting restriction for user ${userId}`);
    }, duration);
  }

  increaseMonitoring(userId) {
    console.log(`Increasing monitoring for user ${userId}`);
    // Implement increased monitoring logic
  }
}

export const intrusionDetection = new IntrusionDetection();

Security Testing and Auditing

Automated Security Testing

// tests/security/SecurityTests.js
import { describe, it, expect } from 'vitest';
import { inputSanitizer } from '../../shared/security/InputSanitizer.js';
import { chessMoveValidator } from '../../shared/security/ChessMoveValidator.js';

describe('Security Tests', () => {
  describe('Input Sanitization', () => {
    it('should sanitize HTML content', () => {
      const maliciousInput = '<script>alert("XSS")</script>';
      const sanitized = inputSanitizer.sanitizeHtml(maliciousInput);

      expect(sanitized).not.toContain('<script>');
      expect(sanitized).toBe('&lt;script&gt;alert(&quot;XSS&quot;)&lt;&#x2F;script&gt;');
    });

    it('should validate chess move notation', () => {
      const validMoves = ['e4', 'Nf3', 'O-O', 'Qh5+', 'Rxe8#'];
      const invalidMoves = ['<script>', 'eval()', 'a9', 'Z1'];

      validMoves.forEach(move => {
        expect(() => inputSanitizer.sanitizeChessMove(move)).not.toThrow();
      });

      invalidMoves.forEach(move => {
        expect(() => inputSanitizer.sanitizeChessMove(move)).toThrow();
      });
    });

    it('should sanitize usernames', () => {
      expect(inputSanitizer.sanitizeUsername('ValidUser')).toBe('ValidUser');
      expect(inputSanitizer.sanitizeUsername('User<script>')).toBe('UserScript()');
      expect(() => inputSanitizer.sanitizeUsername('x')).toThrow();
    });
  });

  describe('Chess Move Validation', () => {
    it('should validate move object structure', () => {
      const validMove = {
        from: 'e2',
        to: 'e4',
        notation: 'e4'
      };

      const invalidMoves = [
        { from: 'e2' }, // Missing 'to'
        { from: 'z1', to: 'e4', notation: 'e4' }, // Invalid square
        { from: 'e2', to: 'e4', notation: '<script>' } // Invalid notation
      ];

      expect(chessMoveValidator.isValidMoveObject(validMove)).toBe(true);

      invalidMoves.forEach(move => {
        expect(chessMoveValidator.isValidMoveObject(move)).toBe(false);
      });
    });
  });

  describe('Token Security', () => {
    it('should detect expired tokens', () => {
      const expiredToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDAwMDAwMDB9.x';
      // Test token expiry detection
    });

    it('should validate token format', () => {
      const validToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
      const invalidTokens = [
        'invalid.token',
        '',
        null,
        'not.a.jwt.token.at.all'
      ];

      // Test token format validation
    });
  });
});

Next Steps

⚠️ **GitHub.com Fallback** ⚠️