Files
Omoluabi/SECURITY_PRIVACY_AUDIT.md

5.6 KiB
Raw Blame History

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:
@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.
  1. Security headers
  • Option A: Static hosting _headers (e.g., Netlify/Vercel):
/*
  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):
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.
  1. 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.
  1. Forms: server validation, CSRF, rate-limit, honeypot
  • Add endpoint example: src/pages/api/contact.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.
  1. 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.
  1. 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.
  1. Secrets and environment hygiene
  • Use import.meta.env and never expose secrets without *_PUBLIC prefix. Add .env.example:
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=
  1. 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.
  1. 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.