122 lines
5.6 KiB
Markdown
122 lines
5.6 KiB
Markdown
### Security & Privacy Audit (EU/GDPR)
|
||
|
||
Scope: Current Astro site; no server endpoints yet; React islands used. Aim: strict security headers, EU analytics, consent only if needed, secrets hygiene, safe forms, and payment integration readiness.
|
||
|
||
---
|
||
|
||
### Executive summary
|
||
- **Strengths**: Static-first Astro; no server auth; minimal third parties.
|
||
- **Gaps**: External Google Fonts (tracking and CSP issues), inline scripts/styles, no security headers configured, forms lack server validation/rate-limiting/CSRF, no cookie/consent model, no analytics choice, donation flow not integrated with EU processor.
|
||
|
||
---
|
||
|
||
### Action checklist (priority)
|
||
|
||
1) Remove Google Fonts; self-host Inter variable
|
||
- File: `src/styles/global.css`
|
||
- Replace `@import` from Google with self-hosted font. Steps:
|
||
- Add `public/fonts/inter/Inter-Variable.woff2`.
|
||
- Add to `src/styles/global.css`:
|
||
```css
|
||
@font-face {
|
||
font-family: 'Inter';
|
||
src: url('/fonts/inter/Inter-Variable.woff2') format('woff2-variations');
|
||
font-weight: 100 900; font-style: normal; font-display: swap;
|
||
}
|
||
```
|
||
- Rationale: Privacy, performance, CSP friendliness.
|
||
|
||
2) Security headers
|
||
- Option A: Static hosting `_headers` (e.g., Netlify/Vercel):
|
||
```text
|
||
/*
|
||
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||
X-Content-Type-Options: nosniff
|
||
Referrer-Policy: strict-origin-when-cross-origin
|
||
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()
|
||
Cross-Origin-Opener-Policy: same-origin
|
||
Cross-Origin-Embedder-Policy: require-corp; report-to="default"
|
||
Cross-Origin-Resource-Policy: same-origin
|
||
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-PLACEHOLDER'; style-src 'self' 'nonce-PLACEHOLDER'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
|
||
```
|
||
- Option B: Nginx snippet (`nginx.conf.example`):
|
||
```nginx
|
||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||
add_header X-Content-Type-Options nosniff;
|
||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()";
|
||
add_header Cross-Origin-Opener-Policy "same-origin";
|
||
add_header Cross-Origin-Embedder-Policy "require-corp";
|
||
add_header Cross-Origin-Resource-Policy "same-origin";
|
||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; style-src 'self' 'nonce-$request_id'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
|
||
```
|
||
- Rationale: Baseline browser protections; strict CSP with nonces for inline modules.
|
||
|
||
3) Inline → external scripts/styles to satisfy CSP
|
||
- Files: `src/layouts/BaseLayout.astro` (inline `<script>`), several pages with inline `style="background..."`.
|
||
- Fix: Move JS to `src/scripts/navigation.ts` and import with `<script type="module" src="/scripts/navigation.js" nonce={Astro.props.nonce} />`; extract inline styles into classes in `src/styles/main.css`.
|
||
|
||
4) Forms: server validation, CSRF, rate-limit, honeypot
|
||
- Add endpoint example: `src/pages/api/contact.ts`
|
||
```ts
|
||
import type { APIRoute } from 'astro';
|
||
import { z } from 'zod';
|
||
|
||
const ContactSchema = z.object({
|
||
name: z.string().min(1),
|
||
email: z.string().email(),
|
||
subject: z.string().min(1),
|
||
message: z.string().min(1),
|
||
website: z.string().max(0).optional(), // honeypot
|
||
});
|
||
|
||
export const POST: APIRoute = async ({ request }) => {
|
||
try {
|
||
const data = await request.json();
|
||
const parsed = ContactSchema.safeParse(data);
|
||
if (!parsed.success) return new Response(JSON.stringify({ ok:false }), { status: 400 });
|
||
// TODO: rate-limit via IP + UA (e.g., in-memory or KV), CSRF via double-submit token header
|
||
// TODO: send via Postmark/Resend EU region
|
||
return new Response(JSON.stringify({ ok:true }), { status: 200 });
|
||
} catch {
|
||
return new Response(JSON.stringify({ ok:false }), { status: 400 });
|
||
}
|
||
};
|
||
```
|
||
- Rationale: GDPR-friendly collection, safe error responses, anti-abuse measures.
|
||
|
||
5) Payments: Mollie integration plan
|
||
- Use server-only handlers for payment creation and webhook signature verification. Store nothing beyond required payment IDs; no card data. Configure allowlisted endpoints in CSP `connect-src` for Mollie API only when enabled.
|
||
|
||
6) Analytics: Plausible (EU) or Matomo (self-host)
|
||
- Recommendation: **Plausible EU** with cookieless, events limited to pageview + donate/volunteer conversions. If consent model needed later, implement prior to enabling cookies.
|
||
|
||
7) Secrets and environment hygiene
|
||
- Use `import.meta.env` and never expose secrets without `*_PUBLIC` prefix. Add `.env.example`:
|
||
```env
|
||
PUBLIC_SITE_URL=
|
||
NODE_ENV=development
|
||
# Analytics
|
||
PUBLIC_PLAUSIBLE_DOMAIN=
|
||
PUBLIC_PLAUSIBLE_SRC=
|
||
# Payments (server-only)
|
||
MOLLIE_API_KEY=
|
||
MOLLIE_WEBHOOK_SECRET=
|
||
# Email provider
|
||
POSTMARK_TOKEN=
|
||
```
|
||
|
||
8) Logging and retention
|
||
- Adopt structured logs without PII; error IDs returned to client; retain logs 30–90 days; no access logs with IP persisted beyond ops needs; document in Privacy Policy.
|
||
|
||
9) DPIA-lite
|
||
- Data categories: contact form (identifiers and message body), newsletter (email), donation (processor-hosted, no card data). Lawful basis: consent/legitimate interest. No special category data processed. Document processor agreements (Plausible EU, Postmark EU region, Mollie EU).
|
||
|
||
---
|
||
|
||
### Next steps
|
||
- Implement headers and CSP; refactor inline code.
|
||
- Add API routes with Zod and anti-abuse; wire contact form to server.
|
||
- Choose analytics (Plausible EU) and configure opt-in text in footer.
|
||
- Prepare Mollie sandbox integration and webhook.
|