Project-Rivet / context /packs /security_patterns.md
HumboldtJoker's picture
Upload folder using huggingface_hub
4554903 verified
|
Raw
History Blame Contribute Delete
4.36 kB

Security Patterns for Multiverse Campus

Environment Variable Safety

// NEVER do this:
const secret = process.env.SECRET || '';  // Empty string bypass!

// ALWAYS do this:
function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    console.error(`FATAL: ${name} is not set. Refusing to start.`);
    process.exit(1);
  }
  return value;
}

const JWT_SECRET = requireEnv('JWT_SECRET');
const WEBHOOK_SECRET = requireEnv('SCHOOL_WEBHOOK_SECRET');

Webhook Signature Verification

import crypto from 'crypto';

function verifyWebhookSignature(payload: Buffer, signature: string, secret: string): boolean {
  // NEVER skip verification — reject if secret is missing
  if (!secret) {
    throw new Error('Webhook secret not configured — rejecting all requests');
  }
  
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

// Route handler
router.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-signature'] as string;
  if (!verifyWebhookSignature(req.body, sig, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  // Process webhook...
});

JWT Best Practices

// Token creation — minimal claims, short expiry
function createAccessToken(student: { id: string; email: string }): string {
  return jwt.sign(
    { sub: student.id, email: student.email },
    JWT_SECRET,
    { expiresIn: '15m', algorithm: 'HS256' }
  );
}

// Token verification — explicit algorithm, clock tolerance
function verifyToken(token: string): JwtPayload {
  return jwt.verify(token, JWT_SECRET, {
    algorithms: ['HS256'],
    clockTolerance: 30, // 30 second clock skew tolerance
  }) as JwtPayload;
}

// NEVER put mutable data (roles, permissions) in the access token
// unless you accept the 15-min staleness window.
// For real-time permission checks, hit the database.

Socket.IO Auth Pattern

io.use(async (socket, next) => {
  const token = socket.handshake.auth?.token;
  if (!token) {
    return next(new Error('Authentication required'));
  }
  
  try {
    const payload = verifyToken(token);
    socket.userId = payload.sub;
    socket.email = payload.email;
    next();
  } catch (err) {
    next(new Error('Invalid or expired token'));
  }
});

// Reconnection: exponential backoff on client
const socket = io(SERVER_URL, {
  auth: { token: getAccessToken() },
  reconnectionDelay: 1000,      // Start at 1s
  reconnectionDelayMax: 30000,  // Cap at 30s
  reconnectionAttempts: 10,     // Give up after 10
});

socket.on('connect_error', async (err) => {
  if (err.message === 'Invalid or expired token') {
    // Only refresh on auth errors, not all errors
    const newToken = await refreshAccessToken();
    socket.auth = { token: newToken };
    socket.connect();
  }
  // For other errors, let the built-in backoff handle it
});

Input Validation

import { z } from 'zod';

// Define schema
const CreateResourceSchema = z.object({
  name: z.string().min(1).max(255),
  type: z.enum(['quest', 'achievement', 'item']),
  value: z.number().int().min(0).max(10000),
});

// Validate in route
router.post('/resource', async (req, res) => {
  const parsed = CreateResourceSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({
      success: false,
      errors: parsed.error.flatten().fieldErrors,
    });
  }
  // parsed.data is fully typed and validated
  const result = await service.create(parsed.data);
  res.json({ success: true, data: result });
});

Rate Limiting

import rateLimit from 'express-rate-limit';

// Global rate limit
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                   // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
});

// Strict limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // Only 5 login attempts per 15 min
  message: { error: 'Too many attempts, please try again later' },
});

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