From 23cd9cb680eb77b94a97266c3353dfc835f0cc69 Mon Sep 17 00:00:00 2001 From: Paul Nothaft <53005142+the-luap@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:40:01 +0100 Subject: [PATCH] fix: address Shannon security assessment findings (37 vulnerabilities) (#254) Remediate 4 Critical, 18 High, 10 Medium, and 5 Low vulnerabilities identified in the Shannon security assessment (2026-03-20). Critical fixes: - Command injection via rsync SSH key path (INJ-VULN-01) - Self-escalation to super_admin role (AUTHZ-VULN-11) - Invite super_admin backdoor (AUTHZ-VULN-12) - Handlebars SSTI in email templates (INJ-VULN-05) Authentication hardening: - Rate limit on share-link login (AUTH-VULN-01) - X-Forwarded-For spoofing bypass (AUTH-VULN-02) - reCAPTCHA fails closed when misconfigured (AUTH-VULN-03) - Token revocation on admin/gallery logout (AUTH-VULN-04/05) - Cookie Secure flag defaults true in production (AUTH-VULN-06) - Remove JWT from admin login response body (AUTH-VULN-07) - Timing-safe gallery slug validation (AUTH-VULN-09) - Account lockout fails closed on DB error (AUTH-VULN-12) - Session endpoint checks token revocation Path traversal & file access: - checksums endpoint path containment (INJ-VULN-03) - manifest validate path containment (INJ-VULN-04) XSS prevention: - Block SVG data URIs in CSS sanitizer (XSS-VULN-01) - Email preview iframe sandbox (XSS-VULN-02) - SSR branding HTML escaping (XSS-VULN-03) - User-Agent sanitization in feedback (XSS-VULN-04) Authorization (IDOR): - Event ownership middleware for all admin routes - Cross-admin user profile read restriction (AUTHZ-VULN-10) SSRF & infrastructure: - Private IP validation for SMTP, S3, rsync hosts - Replace inline JWT with standard adminAuth middleware - CSRF Content-Type enforcement on mutating API endpoints - CSP headers in nginx location blocks Token revocation fix: - Remove overly broad orWhere clause that invalidated all future tokens - Allow empty-body POST requests (logout) in CSRF middleware Co-authored-by: Paul Nothaft --- backend/server.js | 41 ++++++-- backend/src/middleware/ownership.js | 35 +++++++ backend/src/routes/adminArchives.js | 9 +- backend/src/routes/adminBackup.js | 39 ++++++-- backend/src/routes/adminEmail.js | 20 +++- backend/src/routes/adminEvents.js | 17 ++-- backend/src/routes/adminFeedback.js | 6 ++ backend/src/routes/adminPhotos.js | 31 +++--- backend/src/routes/adminUsers.js | 13 ++- backend/src/routes/auth.js | 38 ++++++-- backend/src/routes/galleryFeedback.js | 2 +- backend/src/routes/secureImages.js | 31 +----- backend/src/services/backupService.js | 43 +++++++- backend/src/services/emailProcessor.js | 17 ++-- backend/src/services/rateLimitService.js | 26 +---- backend/src/services/recaptcha.js | 6 +- backend/src/services/storage/s3Storage.js | 10 ++ backend/src/services/userManagementService.js | 37 ++++++- backend/src/utils/authSecurity.js | 2 +- backend/src/utils/cssSanitizer.js | 7 +- backend/src/utils/networkValidation.js | 97 +++++++++++++++++++ backend/src/utils/passwordValidation.js | 22 ++--- backend/src/utils/requestIp.js | 24 +---- backend/src/utils/tokenRevocation.js | 5 - backend/src/utils/tokenUtils.js | 5 +- frontend/nginx.conf | 10 ++ .../components/admin/EmailPreviewModal.tsx | 1 + 27 files changed, 425 insertions(+), 169 deletions(-) create mode 100644 backend/src/middleware/ownership.js create mode 100644 backend/src/utils/networkValidation.js diff --git a/backend/server.js b/backend/server.js index 839e4351..5e977a40 100644 --- a/backend/server.js +++ b/backend/server.js @@ -194,13 +194,23 @@ function composeInlineStyles(payload) { return cssSegments.join('\n\n'); } +function escapeHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + function renderBrandHeader(branding) { - const displayName = branding.companyName || 'PicPeak'; - const logoSrc = branding.logoUrl || '/picpeak-logo-transparent.png'; + const displayName = escapeHtml(branding.companyName || 'PicPeak'); + const logoSrc = encodeURI(branding.logoUrl || '/picpeak-logo-transparent.png'); const logo = ``; const tagline = branding.companyTagline - ? `

${branding.companyTagline}

` + ? `

${escapeHtml(branding.companyTagline)}

` : ''; return `