Refactor theme and language validation to use basic format checks instead of allowlists
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m22s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m22s
This commit is contained in:
@@ -11,7 +11,7 @@ A modern, self-hosted BigBlueButton frontend with beautiful themes, federation,
|
|||||||
|
|
||||||
### Core Features
|
### Core Features
|
||||||
- 🎥 **Video Conferencing** – Integrated BigBlueButton support for professional video meetings
|
- 🎥 **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
|
- 📝 **Room Management** – Create unlimited rooms with custom settings, access codes, and moderator codes
|
||||||
- 🔐 **User Management** – Registration, login, role-based access control (Admin/User)
|
- 🔐 **User Management** – Registration, login, role-based access control (Admin/User)
|
||||||
- 📹 **Recording Management** – View, publish, and delete meeting recordings per room
|
- 📹 **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
|
### Security
|
||||||
- 🛡️ **Comprehensive Rate Limiting** – Login, register, profile, avatar, guest-join, and federation endpoints
|
- 🛡️ **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`
|
- 🕐 **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
|
- 📏 **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
|
- 🧹 **XSS Prevention** – HTML-escaped emails, XML-escaped BBB parameters, SVG sanitization
|
||||||
@@ -186,7 +186,7 @@ redlight/
|
|||||||
│ ├── i18n/ # Translations (DE, EN)
|
│ ├── i18n/ # Translations (DE, EN)
|
||||||
│ ├── pages/ # Page components
|
│ ├── pages/ # Page components
|
||||||
│ ├── services/ # API client
|
│ ├── services/ # API client
|
||||||
│ ├── themes/ # Tailwind theme config (volume-mountable)
|
│ ├── themes/ # Tailwind theme config
|
||||||
│ └── main.jsx # Frontend entry point
|
│ └── main.jsx # Frontend entry point
|
||||||
├── public/ # Static assets
|
├── public/ # Static assets
|
||||||
├── uploads/ # User avatars, branding & presentations (runtime)
|
├── uploads/ # User avatars, branding & presentations (runtime)
|
||||||
@@ -206,7 +206,7 @@ redlight/
|
|||||||
- **Email Verification** – Optional SMTP-based email verification with resend support
|
- **Email Verification** – Optional SMTP-based email verification with resend support
|
||||||
- **CORS Protection** – Restricted to `APP_URL` in production, open in development
|
- **CORS Protection** – Restricted to `APP_URL` in production, open in development
|
||||||
- **Rate Limiting** – Login, register, profile, password, avatar, guest-join, and federation endpoints
|
- **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`
|
- **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
|
- **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`
|
- **XSS / Injection Prevention** – HTML-escaped emails, XML-escaped BBB API parameters, SVG logos served as `attachment`
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import { getDb } from '../config/database.js';
|
|||||||
import redis from '../config/redis.js';
|
import redis from '../config/redis.js';
|
||||||
import { authenticateToken, generateToken } from '../middleware/auth.js';
|
import { authenticateToken, generateToken } from '../middleware/auth.js';
|
||||||
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.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) {
|
if (!process.env.JWT_SECRET) {
|
||||||
console.error('FATAL: JWT_SECRET environment variable is not set.');
|
console.error('FATAL: JWT_SECRET environment variable is not set.');
|
||||||
@@ -35,8 +33,8 @@ function makeRedisStore(prefix) {
|
|||||||
// ── Validation helpers ─────────────────────────────────────────────────────
|
// ── Validation helpers ─────────────────────────────────────────────────────
|
||||||
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
|
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
|
||||||
|
|
||||||
const VALID_THEMES = new Set(themes.map(t => t.id));
|
// Simple format check for theme/language IDs (actual validation happens on the frontend)
|
||||||
const VALID_LANGUAGES = new Set(Object.keys(languages));
|
const SAFE_ID_RE = /^[a-zA-Z0-9_-]{1,50}$/;
|
||||||
|
|
||||||
// Allowlist for CSS color values – only permits hsl(), hex (#rgb/#rrggbb) and plain names
|
// 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})$/;
|
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' });
|
return res.status(400).json({ error: 'Display name must not exceed 100 characters' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// M2: theme and language allowlists
|
// Theme and language: basic format validation (frontend handles actual ID matching)
|
||||||
if (theme !== undefined && theme !== null && !VALID_THEMES.has(theme)) {
|
if (theme !== undefined && theme !== null && (typeof theme !== 'string' || !SAFE_ID_RE.test(theme))) {
|
||||||
return res.status(400).json({ error: 'Invalid 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' });
|
return res.status(400).json({ error: 'Invalid language' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import fs from 'fs';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { getDb } from '../config/database.js';
|
import { getDb } from '../config/database.js';
|
||||||
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
|
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
|
||||||
import { themes } from '../../src/themes/index.js';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const router = Router();
|
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
|
// Ensure uploads/branding directory exists
|
||||||
const brandingDir = path.join(__dirname, '..', '..', 'uploads', 'branding');
|
const brandingDir = path.join(__dirname, '..', '..', 'uploads', 'branding');
|
||||||
@@ -180,8 +179,8 @@ router.put('/default-theme', authenticateToken, requireAdmin, async (req, res) =
|
|||||||
if (!defaultTheme || !defaultTheme.trim()) {
|
if (!defaultTheme || !defaultTheme.trim()) {
|
||||||
return res.status(400).json({ error: 'defaultTheme is required' });
|
return res.status(400).json({ error: 'defaultTheme is required' });
|
||||||
}
|
}
|
||||||
// H4: validate against known theme IDs
|
// Basic format validation for theme ID
|
||||||
if (!VALID_THEMES.has(defaultTheme.trim())) {
|
if (!SAFE_ID_RE.test(defaultTheme.trim())) {
|
||||||
return res.status(400).json({ error: 'Invalid theme' });
|
return res.status(400).json({ error: 'Invalid theme' });
|
||||||
}
|
}
|
||||||
await setSetting('default_theme', defaultTheme.trim());
|
await setSetting('default_theme', defaultTheme.trim());
|
||||||
|
|||||||
Reference in New Issue
Block a user