feat(security): enhance input validation and security measures across various routes
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s

This commit is contained in:
2026-03-04 08:39:29 +01:00
parent ba096a31a2
commit e22a895672
13 changed files with 222 additions and 29 deletions

View File

@@ -14,6 +14,16 @@ const router = Router();
const SAFE_ID_RE = /^[a-zA-Z0-9_-]{1,50}$/;
// Validate that a URL uses a safe scheme (http/https only)
function isSafeUrl(url) {
try {
const parsed = new URL(url);
return parsed.protocol === 'https:' || parsed.protocol === 'http:';
} catch {
return false;
}
}
// Ensure uploads/branding directory exists
const brandingDir = path.join(__dirname, '..', '..', 'uploads', 'branding');
if (!fs.existsSync(brandingDir)) {
@@ -221,6 +231,9 @@ router.put('/imprint-url', authenticateToken, requireAdmin, async (req, res) =>
if (imprintUrl && imprintUrl.length > 500) {
return res.status(400).json({ error: 'URL must not exceed 500 characters' });
}
if (imprintUrl && imprintUrl.trim() && !isSafeUrl(imprintUrl.trim())) {
return res.status(400).json({ error: 'URL must start with http:// or https://' });
}
if (imprintUrl && imprintUrl.trim()) {
await setSetting('imprint_url', imprintUrl.trim());
} else {
@@ -240,6 +253,9 @@ router.put('/privacy-url', authenticateToken, requireAdmin, async (req, res) =>
if (privacyUrl && privacyUrl.length > 500) {
return res.status(400).json({ error: 'URL must not exceed 500 characters' });
}
if (privacyUrl && privacyUrl.trim() && !isSafeUrl(privacyUrl.trim())) {
return res.status(400).json({ error: 'URL must start with http:// or https://' });
}
if (privacyUrl && privacyUrl.trim()) {
await setSetting('privacy_url', privacyUrl.trim());
} else {