Files
Omoluabi/SECURITY_PRIVACY_AUDIT.md

122 lines
5.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### 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 3090 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.