MOODUL 14b · TASE 5 — SPETSIALIST

🔒 Mängu Turvalisus

Serveri-poolne valideerimine, anti-cheat, andmekaitse, XSS/CSRF, turvaline WebSocket — kaitse oma mängu!

⏱️ ~5-7 tundi 📝 5 harjutust 🟣 Spetsialist

🛡️ Samm 1 — Serveri-poolne Valideerimine

🚨 Kuldreegel: Ära usalda klienti!

Kõik, mis tuleb kliendilt, võib olla võltsitud. Klient saadab sisendi (klahvivajutused), server arvutab tulemuse (positsioon, HP, skoor).

serverValidation.ts — Serveri kontrollid
// ======= SERVER (Node.js + Express) =======

interface PlayerState {
  id: string;
  x: number;
  y: number;
  hp: number;
  score: number;
  lastMoveTime: number;
  speed: number; // max lubatud kiirus
}

class GameServer {
  private players: Map<string, PlayerState> = new Map();

  handleMove(playerId: string, input: { dx: number; dy: number }): void {
    const player = this.players.get(playerId);
    if (!player) return;

    const now = Date.now();
    const delta = (now - player.lastMoveTime) / 1000;
    player.lastMoveTime = now;

    // ✅ Valideeri sisend
    const dx = Math.max(-1, Math.min(1, input.dx)); // Clamp -1..1
    const dy = Math.max(-1, Math.min(1, input.dy));

    // ✅ Kiiruse kontroll
    const moveX = dx * player.speed * delta;
    const moveY = dy * player.speed * delta;
    const distance = Math.sqrt(moveX * moveX + moveY * moveY);
    const maxDistance = player.speed * delta * 1.1; // 10% tolerants

    if (distance > maxDistance) {
      console.warn(`[CHEAT] Mängija ${playerId}: liiga kiire liikumine!`);
      this.flagPlayer(playerId, 'speed_hack');
      return;
    }

    // ✅ Seinade kontroll
    const newX = player.x + moveX;
    const newY = player.y + moveY;
    if (this.isWalkable(newX, newY)) {
      player.x = newX;
      player.y = newY;
    }
  }

  handleDamage(attackerId: string, targetId: string): void {
    const attacker = this.players.get(attackerId);
    const target = this.players.get(targetId);
    if (!attacker || !target) return;

    // ✅ Kauguse kontroll
    const dist = Math.hypot(attacker.x - target.x, attacker.y - target.y);
    if (dist > 3) { // Max rünnaku kaugus
      console.warn(`[CHEAT] ${attackerId}: ründab kaugelt (${dist})`);
      return;
    }

    // ✅ Cooldown kontroll
    // ✅ Server arvutab damage, MITTE klient
    const damage = this.calculateDamage(attacker);
    target.hp = Math.max(0, target.hp - damage);
  }

  private flagPlayer(id: string, reason: string): void {
    // Loggi, ära bänni kohe (false positives)
    console.log(`[ANTI-CHEAT] Flag: ${id} — ${reason}`);
  }

  private isWalkable(x: number, y: number): boolean { return true; }
  private calculateDamage(attacker: PlayerState): number { return 10; }
}

🔐 Samm 2 — Anti-Cheat Meetodid

antiCheat.ts
class AntiCheatSystem {
  private violations: Map<string, number> = new Map();
  private readonly MAX_VIOLATIONS = 5;

  // 1. Kiiruse kontroll (speedhack tuvastamine)
  checkSpeed(player: PlayerState, newPos: { x: number; y: number }, delta: number): boolean {
    const dist = Math.hypot(newPos.x - player.x, newPos.y - player.y);
    const maxDist = player.speed * delta * 1.2; // 20% tolerants latency jaoks
    if (dist > maxDist) {
      this.addViolation(player.id, 'speed');
      return false;
    }
    return true;
  }

  // 2. Teleporti tuvastamine
  checkTeleport(player: PlayerState, newPos: { x: number; y: number }): boolean {
    const dist = Math.hypot(newPos.x - player.x, newPos.y - player.y);
    if (dist > 50) { // Mitte keegi ei liigu 50 ühikut ühe frame'iga
      this.addViolation(player.id, 'teleport');
      return false;
    }
    return true;
  }

  // 3. Sõnumite sageduse kontroll (firerate hack)
  private messageTimestamps: Map<string, number[]> = new Map();
  
  checkMessageRate(playerId: string, maxPerSecond = 30): boolean {
    const now = Date.now();
    if (!this.messageTimestamps.has(playerId)) {
      this.messageTimestamps.set(playerId, []);
    }
    const timestamps = this.messageTimestamps.get(playerId)!;
    timestamps.push(now);

    // Eemalda vanemad kui 1s
    while (timestamps.length > 0 && timestamps[0] < now - 1000) {
      timestamps.shift();
    }

    if (timestamps.length > maxPerSecond) {
      this.addViolation(playerId, 'message_flood');
      return false;
    }
    return true;
  }

  // 4. HP manipulatsiooni tuvastamine
  checkHealth(player: PlayerState, reportedHp: number): boolean {
    if (reportedHp > player.hp) {
      this.addViolation(player.id, 'hp_hack');
      return false; // HP ei saa kasvada ilma heal'imata
    }
    return true;
  }

  private addViolation(playerId: string, type: string): void {
    const count = (this.violations.get(playerId) || 0) + 1;
    this.violations.set(playerId, count);
    console.warn(`[ANTI-CHEAT] ${playerId}: ${type} (${count}/${this.MAX_VIOLATIONS})`);

    if (count >= this.MAX_VIOLATIONS) {
      this.banPlayer(playerId);
    }
  }

  private banPlayer(playerId: string): void {
    console.log(`[BAN] Mängija ${playerId} banned!`);
    // Disconnect + blacklist
  }
}

🌐 Samm 3 — Veebiturvalisus

webSecurity.ts — XSS, CSRF, CSP
// ====== 1. XSS KAITSE ======
// HALB: Mängija nimi otse HTML-i
// element.innerHTML = player.name; // ❌ <script>hack()</script>

// HEA: Sanitiseeri sisend
function sanitize(input: string): string {
  const div = document.createElement('div');
  div.textContent = input; // Automaatne escaping
  return div.innerHTML;
}

// Või kasuta DOMPurify:
// import DOMPurify from 'dompurify';
// element.innerHTML = DOMPurify.sanitize(player.name);

// ====== 2. TURVALINE WebSocket ======
import { WebSocketServer } from 'ws';
import jwt from 'jsonwebtoken';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
  // Autentimise token päisest
  const token = new URL(req.url!, 'http://localhost').searchParams.get('token');
  
  try {
    const payload = jwt.verify(token!, process.env.JWT_SECRET!);
    ws.userId = (payload as any).userId;
  } catch {
    ws.close(4001, 'Unauthorized');
    return;
  }

  // Rate limiting
  let messageCount = 0;
  const resetInterval = setInterval(() => { messageCount = 0; }, 1000);

  ws.on('message', (raw) => {
    messageCount++;
    if (messageCount > 60) {
      ws.close(4002, 'Rate limit exceeded');
      return;
    }

    // Valideeri JSON
    try {
      const msg = JSON.parse(raw.toString());
      if (!isValidMessage(msg)) throw new Error('Invalid format');
      handleMessage(ws, msg);
    } catch {
      ws.close(4003, 'Invalid message');
    }
  });

  ws.on('close', () => clearInterval(resetInterval));
});

// ====== 3. CSP HEADER ======
// Express middleware:
// app.use((req, res, next) => {
//   res.setHeader('Content-Security-Policy',
//     "default-src 'self'; " +
//     "script-src 'self'; " +      // Ainult oma skriptid
//     "style-src 'self' 'unsafe-inline'; " +
//     "img-src 'self' data: blob:; " +
//     "connect-src 'self' wss://yourserver.com"
//   );
//   next();
// });

📝 Harjutused

  • Serveri-poolne valideerimine

    Loo lihtne mänguserver: mängija liikumine ainult serveris. Klient saadab input, server vastab positsiooniga. Testi DevToolsiga manipuleerimist.

  • Anti-cheat süsteem

    Loo AntiCheat: speed check, teleport detection, rate limiting. Violation counter → ban. Testi: modifitseeri kliendi koodi ja vaata kas server tuvastab.

  • Turvaline chat süsteem

    Mängusisene chat: XSS kaitse (sanitize), profanity filter, rate limiting (max 5 sõnumit/10s). Serveri-poolne logi. Mute funktsioon.

  • JWT autentimine mängus

    Login → JWT token → WebSocket autentimine. Token expiry + refresh. Turvaline salvestamine (httpOnly cookie vs memory). Logout kõigist seadmetest.

  • Skooride turvaline salvestamine

    Leaderboard: skoorid ainult serveris. Klient saadab mängutegevused, server arvutab skoori. HMAC allkiri lisaturvalisuseks. Replay valideerimine.

← Moodul 14 Moodul 15: Publitseerimine →