## 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
@@
-