5.6 KiB
5.6 KiB
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)
- 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
:
- Add
@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.
- 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.
- Inline → external scripts/styles to satisfy CSP
- Files:
src/layouts/BaseLayout.astro
(inline<script>
), several pages with inlinestyle="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 insrc/styles/main.css
.
- 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.
- 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.
- 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.
- 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=
- 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.
- 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.