Files
365devnet/SECURITY_AUDIT.md
Richard Bergsma a767dbb115 Enhance security and localization features across the application
- Added rehype-sanitize plugin to the markdown configuration for improved security against XSS attacks.
- Updated environment variables in the codebase to include new configurations for SMTP and monitoring.
- Implemented secure headers in server and Nginx configurations to bolster security.
- Refactored email handling to prevent spoofing by ensuring safe sender addresses.
- Improved localization by updating language persistence and button components for better user experience.
- Enhanced the uptime API and contact form with better error handling and logging practices.
- Updated dependencies in package.json and package-lock.json for better performance and security.
2025-10-19 21:13:15 +02:00

5.5 KiB

Executive summary

Astro SSR is enabled; endpoints handle contact, uptime, and commits. Main risks were HTML injection and missing headers. Minimal edits applied: enable rehype-sanitize, drop Mermaid CDN, avoid innerHTML where not needed, reduce PII in logs, soften Gemini init, and add headers/CSP snippets for both static hosts and Nginx.

Risk table

ID File/Path Issue Severity Why it matters Fix
S1 astro.config.ts MD/MDX not sanitized High XSS via content/slots Add rehype-sanitize
S2 src/pages/[lang]/topdesk.astro CDN Mermaid + innerHTML High CSP bypass/XSS Use local mermaid, keep injection controlled
S3 src/components/widgets/ModernEducation.astro Title via innerHTML Med XSS if title contains HTML Use textContent
S4 src/pages/[lang]/index.astro set:html for line breaks Low Avoidable HTML injection Render lines with <br/>
S5 src/pages/api/contact.ts PII in logs Med Privacy/legal Remove PII logs
S6 src/utils/gemini-spam-check.ts Throws when key missing Med Startup failure Lazy init, fail open
S7 public/_headers, nginx/nginx.conf Missing headers/CSP High XSS/MIME sniffing Add strict headers and CSP
S8 src/middleware.ts 301 for lang redirect Low Sticky cache Use 302
S9 src/pages/api/uptime.ts Verbose error detail Low Info leakage Return generic details

Detailed findings with references

  • S1: astro.config.ts now includes rehype-sanitize alongside existing plugins.
  • S2: src/pages/[lang]/topdesk.astro imports mermaid locally and renders SVG without remote CDN.
  • S3/S4: Components/pages adjusted to avoid innerHTML/set:html for simple cases.
  • S5/S6/S9: API/util logs are minimal; no PII or stack traces in prod responses.
  • S7: Headers added for both Nginx and static hosting.
  • S8: Middleware redirect switched to 302.

Applied patches (concise)

  1. Enable sanitize in Astro markdown
--- a/astro.config.ts
+++ b/astro.config.ts
@@
   markdown: {
     remarkPlugins: [readingTimeRemarkPlugin],
-    rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
+    rehypePlugins: [rehypeSanitize, responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
   },
  1. Remove CDN and render Mermaid locally
--- a/src/pages/[lang]/topdesk.astro
+++ b/src/pages/[lang]/topdesk.astro
@@
-      const mermaid = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');
+      const mermaidModule = await import('mermaid');
+      const mermaid = mermaidModule.default || mermaidModule;
@@
-          const { svg } = await mermaid.default.render(id, mermaidCode);
+          const { svg } = await mermaid.render(id, mermaidCode);
  1. Avoid innerHTML for titles; use textContent
--- a/src/components/widgets/ModernEducation.astro
+++ b/src/components/widgets/ModernEducation.astro
@@
-      const tempDiv = document.createElement('div');
-      tempDiv.innerHTML = title;
-      const cleanTitle = tempDiv.textContent || tempDiv.innerText || title;
+      const cleanTitle = String(title);
@@
-      document.getElementById('educationModalTitle').innerHTML = title;
+      const titleEl = document.getElementById('educationModalTitle');
+      if (titleEl) titleEl.textContent = cleanTitle;
  1. Replace trivial set:html
--- a/src/pages/[lang]/index.astro
+++ b/src/pages/[lang]/index.astro
@@
-          <div class="text-lg pt-4 method-highlights" set:html={explosive.approach.highlights.replace(/\n/g, '<br/>')}>
-          </div>
+          <div class="text-lg pt-4 method-highlights">
+            {explosive.approach.highlights.split('\n').map((line) => (
+              <>
+                {line}
+                <br />
+              </>
+            ))}
+          </div>
  1. Reduce PII logging; lazy Gemini init
--- a/src/pages/api/contact.ts
+++ b/src/pages/api/contact.ts
@@
-    console.log('Client IP:', ipAddress);
+    // avoid logging IP in production
@@
-      console.log('Rate limit exceeded');
+      console.warn('Rate limit exceeded for contact endpoint');
--- a/src/utils/gemini-spam-check.ts
+++ b/src/utils/gemini-spam-check.ts
@@
-const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
-if (!GEMINI_API_KEY) { throw new Error('...') }
-const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
+const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
+const genAI = GEMINI_API_KEY ? new GoogleGenerativeAI(GEMINI_API_KEY) : null;
  1. Headers/CSP
  • Static host _headers additions in public/_headers (applied).
  • Nginx secure headers added in nginx/nginx.conf (applied).

Starter CSP (static host file already contains a commented safer baseline):

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.pixabay.com https://raw.githubusercontent.com; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'

If you keep inline scripts, use Astro nonces or move to external files to avoid 'unsafe-inline' for scripts entirely.

Follow-up tasks

  • Add CI with: npm ci, npm run check, npm run build, npm audit --omit=dev.
  • Consider switching output: 'server' to prerender where feasible for brochure pages; keep APIs SSR.
  • Review all remaining set:html occurrences and ensure content sources are trusted/sanitized.
  • Document .env variables; .env example added to README.