Security Best Practices
Multiplayer games face unique security challenges. This guide covers essential security practices for martini-kit games.
1. Input Validation
Always validate all input on the host/server:
actions: {
move: {
apply(state, context, input: { x: number; y: number }) {
// ✅ Validate bounds
if (input.x < 0 || input.x > 800 || input.y < 0 || input.y > 600) {
console.warn('Invalid move input from', context.playerId);
return; // Reject invalid input
}
// ✅ Validate speed (anti-cheat)
const player = state.players[context.targetId];
const distance = Math.hypot(input.x - player.x, input.y - player.y);
const maxSpeed = 10;
if (distance > maxSpeed) {
console.warn('Speed hack detected from', context.playerId);
// Clamp to max speed
const ratio = maxSpeed / distance;
input.x = player.x + (input.x - player.x) * ratio;
input.y = player.y + (input.y - player.y) * ratio;
}
player.x = input.x;
player.y = input.y;
}
}
} 2. Rate Limiting
Prevent action spam:
// Server-side rate limiting
const rateLimits = new Map<string, { count: number; resetTime: number }>();
function checkRateLimit(playerId: string): boolean {
const now = Date.now();
const limit = rateLimits.get(playerId);
if (!limit || now > limit.resetTime) {
rateLimits.set(playerId, {
count: 1,
resetTime: now + 1000 // 1 second window
});
return true;
}
if (limit.count >= 100) { // 100 actions per second max
return false;
}
limit.count++;
return true;
}
// In WebSocket message handler
ws.on('message', (data) => {
const message = JSON.parse(data);
if (!checkRateLimit(playerId)) {
console.warn('Rate limit exceeded for', playerId);
ws.close(1008, 'Rate limit exceeded');
return;
}
// Process message...
}); 3. Sanitize User Input
Prevent XSS attacks in player names and chat:
function sanitizeName(name: string): string {
return name
.replace(/[<>]/g, '') // Remove HTML brackets
.substring(0, 20) // Limit length
.trim();
}
actions: {
setName: {
apply(state, context, input: { name: string }) {
const player = state.players[context.targetId];
player.name = sanitizeName(input.name);
}
}
} 4. Use HTTPS/WSS in Production
Never use insecure connections in production:
// ✅ Correct - secure connections
const wsUrl = import.meta.env.PROD
? 'wss://your-server.com' // WSS in production
: 'ws://localhost:8080'; // WS in development
// ❌ Wrong - insecure in production
const wsUrl = 'ws://your-server.com'; 5. Environment Variables
Never commit secrets to version control:
// config.ts
export const config = {
wsUrl: import.meta.env.VITE_WS_URL,
turnUrl: import.meta.env.VITE_TURN_URL,
turnUsername: import.meta.env.VITE_TURN_USERNAME,
turnCredential: import.meta.env.VITE_TURN_CREDENTIAL,
}; .env.production (not committed):
VITE_WS_URL=wss://your-server.com
VITE_TURN_URL=turn:your-turn.com:3478
VITE_TURN_USERNAME=production_user
VITE_TURN_CREDENTIAL=secret_password .gitignore:
.env.production
.env.local 6. Server-Authoritative Logic
For competitive games, run critical logic on the server:
// ❌ Client-authoritative (can be cheated)
actions: {
takeDamage: {
apply(state, context, input: { damage: number }) {
// Client can send any damage value!
state.players[context.targetId].health -= input.damage;
}
}
}
// ✅ Server-authoritative (safe)
actions: {
shoot: {
apply(state, context, input: { targetId: string }) {
// Server calculates damage
const shooter = state.players[context.playerId];
const target = state.players[input.targetId];
// Server validates hit
const distance = Math.hypot(
shooter.x - target.x,
shooter.y - target.y
);
if (distance > shooter.weaponRange) {
return; // Out of range
}
// Server determines damage
const damage = shooter.weaponDamage;
target.health -= damage;
}
}
} 7. Prevent Replay Attacks
Add timestamps to actions:
interface ActionMessage {
actionName: string;
input: any;
timestamp: number;
}
// Server validates timestamp
function validateTimestamp(timestamp: number): boolean {
const now = Date.now();
const maxAge = 5000; // 5 seconds
return Math.abs(now - timestamp) < maxAge;
} 8. Secure WebSocket Connections
// Server-side (Node.js)
import { WebSocketServer } from 'ws';
import https from 'https';
import fs from 'fs';
const server = https.createServer({
cert: fs.readFileSync('/path/to/cert.pem'),
key: fs.readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
// Verify origin
const origin = req.headers.origin;
if (origin !== 'https://your-game.com') {
ws.close(1008, 'Invalid origin');
return;
}
// ... handle connection
});
server.listen(443); 9. Monitor for Abuse
Track suspicious behavior:
const suspiciousActivity = new Map<string, number>();
function trackSuspiciousActivity(playerId: string, reason: string) {
const count = (suspiciousActivity.get(playerId) || 0) + 1;
suspiciousActivity.set(playerId, count);
console.warn(`Suspicious activity from ${playerId}: ${reason} (count: ${count})`);
if (count >= 5) {
// Ban player
console.error(`Banning player ${playerId} for repeated violations`);
// Implement ban logic
}
}
// Use in actions
if (invalidInput) {
trackSuspiciousActivity(context.playerId, 'invalid input');
return;
} 10. CORS Configuration
Restrict which origins can connect:
// Server-side
const allowedOrigins = [
'https://your-game.com',
'https://www.your-game.com'
];
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
if (!allowedOrigins.includes(origin)) {
ws.close(1008, 'Origin not allowed');
return;
}
}); Security Checklist
Before deploying to production:
- All input validated on host/server
- Rate limiting implemented
- User input sanitized (names, chat)
- HTTPS/WSS only in production
- Secrets in environment variables (not committed)
- Critical logic server-authoritative
- Timestamps validated
- WebSocket connections secured
- Monitoring for abuse
- CORS properly configured
See Also
- Choosing a Transport → - Transport options
- Testing → - Security testing