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

This commit is contained in:
2026-02-28 20:30:11 +01:00
parent c281628fdc
commit 1cff066c17
3 changed files with 12 additions and 15 deletions

View File

@@ -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' });
}

View File

@@ -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());