- 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.
5.5 KiB
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 includesrehype-sanitize
alongside existing plugins. - S2:
src/pages/[lang]/topdesk.astro
importsmermaid
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)
- Enable sanitize in Astro markdown
--- a/astro.config.ts
+++ b/astro.config.ts
@@
markdown: {
remarkPlugins: [readingTimeRemarkPlugin],
- rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
+ rehypePlugins: [rehypeSanitize, responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
},
- 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);
- Avoid
innerHTML
for titles; usetextContent
--- 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;
- 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>
- 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;
- Headers/CSP
- Static host
_headers
additions inpublic/_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.