## 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 `
` | | 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 ```diff --- a/astro.config.ts +++ b/astro.config.ts @@ markdown: { remarkPlugins: [readingTimeRemarkPlugin], - rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin], + rehypePlugins: [rehypeSanitize, responsiveTablesRehypePlugin, lazyImagesRehypePlugin], }, ``` 2) Remove CDN and render Mermaid locally ```diff --- 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); ``` 3) Avoid `innerHTML` for titles; use `textContent` ```diff --- 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; ``` 4) Replace trivial `set:html` ```diff --- a/src/pages/[lang]/index.astro +++ b/src/pages/[lang]/index.astro @@ -
')}> -
+
+ {explosive.approach.highlights.split('\n').map((line) => ( + <> + {line} +
+ + ))} +
``` 5) Reduce PII logging; lazy Gemini init ```diff --- 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'); ``` ```diff --- 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; ``` 6) 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): ```text 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.