docs: add ACCESSIBILITY_AUDIT, SECURITY_PRIVACY_AUDIT, PRODUCT_BRIEF with file-specific actions; a11y: add skip link, lang+hreflang, ARIA for menu/carousel/dialog/form; noValidate+honeypot

This commit is contained in:
2025-08-08 23:00:09 +02:00
parent 910253c8f4
commit 3c74d71e22
16 changed files with 501 additions and 43 deletions

121
SECURITY_PRIVACY_AUDIT.md Normal file
View File

@@ -0,0 +1,121 @@
### 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.