Enhance security and validation across multiple routes:
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m25s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m25s
- Escape XML and HTML special characters to prevent injection attacks. - Implement rate limiting for various endpoints to mitigate abuse. - Add validation for email formats, password lengths, and field limits. - Ensure proper access control for recordings and room management.
This commit is contained in:
@@ -3,6 +3,7 @@ import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { authenticateToken } from '../middleware/auth.js';
|
||||
import {
|
||||
@@ -13,11 +14,29 @@ import {
|
||||
isMeetingRunning,
|
||||
} from '../config/bbb.js';
|
||||
|
||||
// L6: constant-time string comparison for access/moderator codes
|
||||
function timingSafeEqual(a, b) {
|
||||
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
||||
const bufA = Buffer.from(a);
|
||||
const bufB = Buffer.from(b);
|
||||
if (bufA.length !== bufB.length) return false;
|
||||
return crypto.timingSafeEqual(bufA, bufB);
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const presentationsDir = path.join(__dirname, '..', '..', 'uploads', 'presentations');
|
||||
if (!fs.existsSync(presentationsDir)) fs.mkdirSync(presentationsDir, { recursive: true });
|
||||
|
||||
// M8: rate limit unauthenticated guest-join to prevent access_code brute-force
|
||||
const guestJoinLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 15,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many join attempts. Please try again later.' },
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Build avatar URL for a user (uploaded image or generated initials)
|
||||
@@ -140,6 +159,20 @@ router.post('/', authenticateToken, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Room name is required' });
|
||||
}
|
||||
|
||||
// M7: field length limits
|
||||
if (name.trim().length > 100) {
|
||||
return res.status(400).json({ error: 'Room name must not exceed 100 characters' });
|
||||
}
|
||||
if (welcome_message && welcome_message.length > 2000) {
|
||||
return res.status(400).json({ error: 'Welcome message must not exceed 2000 characters' });
|
||||
}
|
||||
if (access_code && access_code.length > 50) {
|
||||
return res.status(400).json({ error: 'Access code must not exceed 50 characters' });
|
||||
}
|
||||
if (moderator_code && moderator_code.length > 50) {
|
||||
return res.status(400).json({ error: 'Moderator code must not exceed 50 characters' });
|
||||
}
|
||||
|
||||
const uid = crypto.randomBytes(8).toString('hex');
|
||||
const db = getDb();
|
||||
|
||||
@@ -194,6 +227,20 @@ router.put('/:uid', authenticateToken, async (req, res) => {
|
||||
moderator_code,
|
||||
} = req.body;
|
||||
|
||||
// M12: field length limits (same as create)
|
||||
if (name && name.trim().length > 100) {
|
||||
return res.status(400).json({ error: 'Room name must not exceed 100 characters' });
|
||||
}
|
||||
if (welcome_message && welcome_message.length > 2000) {
|
||||
return res.status(400).json({ error: 'Welcome message must not exceed 2000 characters' });
|
||||
}
|
||||
if (access_code && access_code.length > 50) {
|
||||
return res.status(400).json({ error: 'Access code must not exceed 50 characters' });
|
||||
}
|
||||
if (moderator_code && moderator_code.length > 50) {
|
||||
return res.status(400).json({ error: 'Moderator code must not exceed 50 characters' });
|
||||
}
|
||||
|
||||
await db.run(`
|
||||
UPDATE rooms SET
|
||||
name = COALESCE(?, name),
|
||||
@@ -376,7 +423,7 @@ router.post('/:uid/join', authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
// Check access code if set
|
||||
if (room.access_code && req.body.access_code !== room.access_code) {
|
||||
if (room.access_code && !timingSafeEqual(req.body.access_code || '', room.access_code)) {
|
||||
return res.status(403).json({ error: 'Wrong access code' });
|
||||
}
|
||||
|
||||
@@ -464,7 +511,7 @@ router.get('/:uid/public', async (req, res) => {
|
||||
});
|
||||
|
||||
// POST /api/rooms/:uid/guest-join - Join meeting as guest (no auth needed)
|
||||
router.post('/:uid/guest-join', async (req, res) => {
|
||||
router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
|
||||
try {
|
||||
const { name, access_code, moderator_code } = req.body;
|
||||
|
||||
@@ -472,6 +519,11 @@ router.post('/:uid/guest-join', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Name is required' });
|
||||
}
|
||||
|
||||
// L1: limit guest name length
|
||||
if (name.trim().length > 100) {
|
||||
return res.status(400).json({ error: 'Name must not exceed 100 characters' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
|
||||
|
||||
@@ -480,7 +532,7 @@ router.post('/:uid/guest-join', async (req, res) => {
|
||||
}
|
||||
|
||||
// Check access code if set
|
||||
if (room.access_code && access_code !== room.access_code) {
|
||||
if (room.access_code && !timingSafeEqual(access_code || '', room.access_code)) {
|
||||
return res.status(403).json({ error: 'Wrong access code' });
|
||||
}
|
||||
|
||||
@@ -499,7 +551,7 @@ router.post('/:uid/guest-join', async (req, res) => {
|
||||
|
||||
// Check moderator code
|
||||
let isModerator = !!room.all_join_moderator;
|
||||
if (!isModerator && moderator_code && room.moderator_code && moderator_code === room.moderator_code) {
|
||||
if (!isModerator && moderator_code && room.moderator_code && timingSafeEqual(moderator_code, room.moderator_code)) {
|
||||
isModerator = true;
|
||||
}
|
||||
|
||||
@@ -542,13 +594,30 @@ router.post('/:uid/presentation', authenticateToken, async (req, res) => {
|
||||
const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
|
||||
if (!room) return res.status(404).json({ error: 'Room not found or no permission' });
|
||||
|
||||
// M16: stream-level size limit – abort as soon as 50 MB is exceeded
|
||||
const MAX_PRESENTATION_SIZE = 50 * 1024 * 1024;
|
||||
const buffer = await new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
req.on('data', chunk => chunks.push(chunk));
|
||||
let totalSize = 0;
|
||||
req.on('data', chunk => {
|
||||
totalSize += chunk.length;
|
||||
if (totalSize > MAX_PRESENTATION_SIZE) {
|
||||
req.destroy();
|
||||
return reject(new Error('LIMIT_EXCEEDED'));
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
req.on('error', reject);
|
||||
}).catch(err => {
|
||||
if (err.message === 'LIMIT_EXCEEDED') return null;
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (!buffer) {
|
||||
return res.status(400).json({ error: 'File must not exceed 50MB' });
|
||||
}
|
||||
|
||||
const contentType = req.headers['content-type'] || '';
|
||||
const extMap = {
|
||||
'application/pdf': 'pdf',
|
||||
@@ -561,9 +630,6 @@ router.post('/:uid/presentation', authenticateToken, async (req, res) => {
|
||||
const ext = extMap[contentType];
|
||||
if (!ext) return res.status(400).json({ error: 'Unsupported file type. Allowed: PDF, PPT, PPTX, ODP, DOC, DOCX' });
|
||||
|
||||
// Max 50MB
|
||||
if (buffer.length > 50 * 1024 * 1024) return res.status(400).json({ error: 'File must not exceed 50MB' });
|
||||
|
||||
// Preserve original filename (sent as X-Filename header)
|
||||
const rawName = req.headers['x-filename'];
|
||||
const originalName = rawName
|
||||
|
||||
Reference in New Issue
Block a user