From 1cff066c178498bf7131d427ef044def27a991ad Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 28 Feb 2026 20:30:11 +0100 Subject: [PATCH] Refactor theme and language validation to use basic format checks instead of allowlists --- README.md | 8 ++++---- server/routes/auth.js | 12 +++++------- server/routes/branding.js | 7 +++---- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 51ee5e9..a4c5ea6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A modern, self-hosted BigBlueButton frontend with beautiful themes, federation, ### Core Features - πŸŽ₯ **Video Conferencing** – Integrated BigBlueButton support for professional video meetings -- 🎨 **15+ Themes** – Dracula, Nord, Catppuccin, RosΓ© Pine, Gruvbox, and more (extensible via volume mount) +- 🎨 **15+ Themes** – Dracula, Nord, Catppuccin, RosΓ© Pine, Gruvbox, and more - πŸ“ **Room Management** – Create unlimited rooms with custom settings, access codes, and moderator codes - πŸ” **User Management** – Registration, login, role-based access control (Admin/User) - πŸ“Ή **Recording Management** – View, publish, and delete meeting recordings per room @@ -42,7 +42,7 @@ A modern, self-hosted BigBlueButton frontend with beautiful themes, federation, ### Security - πŸ›‘οΈ **Comprehensive Rate Limiting** – Login, register, profile, avatar, guest-join, and federation endpoints -- πŸ”’ **Input Validation** – Email format, field length limits, theme/language allowlists, color format validation +- πŸ”’ **Input Validation** – Email format, field length limits, ID format checks, color format validation - πŸ• **Timing-Safe Comparisons** – Access codes and moderator codes compared with `crypto.timingSafeEqual` - πŸ“ **Streaming Upload Limits** – Avatar (5 MB) and presentation (50 MB) uploads reject early without buffering - 🧹 **XSS Prevention** – HTML-escaped emails, XML-escaped BBB parameters, SVG sanitization @@ -186,7 +186,7 @@ redlight/ β”‚ β”œβ”€β”€ i18n/ # Translations (DE, EN) β”‚ β”œβ”€β”€ pages/ # Page components β”‚ β”œβ”€β”€ services/ # API client -β”‚ β”œβ”€β”€ themes/ # Tailwind theme config (volume-mountable) +β”‚ β”œβ”€β”€ themes/ # Tailwind theme config β”‚ └── main.jsx # Frontend entry point β”œβ”€β”€ public/ # Static assets β”œβ”€β”€ uploads/ # User avatars, branding & presentations (runtime) @@ -206,7 +206,7 @@ redlight/ - **Email Verification** – Optional SMTP-based email verification with resend support - **CORS Protection** – Restricted to `APP_URL` in production, open in development - **Rate Limiting** – Login, register, profile, password, avatar, guest-join, and federation endpoints -- **Input Validation** – Email regex, field length limits, theme/language allowlists, hex-color format checks +- **Input Validation** – Email regex, field length limits, ID format checks, hex-color format checks - **Timing-Safe Comparisons** – Access codes and moderator codes compared via `crypto.timingSafeEqual` - **Upload Safety** – Streaming body size limits (avatar 5 MB, presentation 50 MB) abort early without buffering - **XSS / Injection Prevention** – HTML-escaped emails, XML-escaped BBB API parameters, SVG logos served as `attachment` diff --git a/server/routes/auth.js b/server/routes/auth.js index ec74c91..7804fc0 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -11,8 +11,6 @@ import { getDb } from '../config/database.js'; import redis from '../config/redis.js'; import { authenticateToken, generateToken } from '../middleware/auth.js'; import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js'; -import { themes } from '../../src/themes/index.js'; -import { languages } from '../../src/i18n/index.js'; if (!process.env.JWT_SECRET) { console.error('FATAL: JWT_SECRET environment variable is not set.'); @@ -35,8 +33,8 @@ function makeRedisStore(prefix) { // ── Validation helpers ───────────────────────────────────────────────────── const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/; -const VALID_THEMES = new Set(themes.map(t => t.id)); -const VALID_LANGUAGES = new Set(Object.keys(languages)); +// Simple format check for theme/language IDs (actual validation happens on the frontend) +const SAFE_ID_RE = /^[a-zA-Z0-9_-]{1,50}$/; // Allowlist for CSS color values – only permits hsl(), hex (#rgb/#rrggbb) and plain names const SAFE_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|hsl\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%\)|[a-zA-Z]{1,30})$/; @@ -383,11 +381,11 @@ router.put('/profile', authenticateToken, profileLimiter, async (req, res) => { return res.status(400).json({ error: 'Display name must not exceed 100 characters' }); } - // M2: theme and language allowlists - if (theme !== undefined && theme !== null && !VALID_THEMES.has(theme)) { + // Theme and language: basic format validation (frontend handles actual ID matching) + if (theme !== undefined && theme !== null && (typeof theme !== 'string' || !SAFE_ID_RE.test(theme))) { return res.status(400).json({ error: 'Invalid theme' }); } - if (language !== undefined && language !== null && !VALID_LANGUAGES.has(language)) { + if (language !== undefined && language !== null && (typeof language !== 'string' || !SAFE_ID_RE.test(language))) { return res.status(400).json({ error: 'Invalid language' }); } diff --git a/server/routes/branding.js b/server/routes/branding.js index 452b2ac..330a2cd 100644 --- a/server/routes/branding.js +++ b/server/routes/branding.js @@ -5,14 +5,13 @@ import fs from 'fs'; import { fileURLToPath } from 'url'; import { getDb } from '../config/database.js'; import { authenticateToken, requireAdmin } from '../middleware/auth.js'; -import { themes } from '../../src/themes/index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const router = Router(); -const VALID_THEMES = new Set(themes.map(t => t.id)); +const SAFE_ID_RE = /^[a-zA-Z0-9_-]{1,50}$/; // Ensure uploads/branding directory exists const brandingDir = path.join(__dirname, '..', '..', 'uploads', 'branding'); @@ -180,8 +179,8 @@ router.put('/default-theme', authenticateToken, requireAdmin, async (req, res) = if (!defaultTheme || !defaultTheme.trim()) { return res.status(400).json({ error: 'defaultTheme is required' }); } - // H4: validate against known theme IDs - if (!VALID_THEMES.has(defaultTheme.trim())) { + // Basic format validation for theme ID + if (!SAFE_ID_RE.test(defaultTheme.trim())) { return res.status(400).json({ error: 'Invalid theme' }); } await setSetting('default_theme', defaultTheme.trim());