Security Checklist

Defense-in-depth security — controleer dit voor elk deploy.

Authenticatie & Autorisatie

  • Supabase Auth — Email + magic link (geen wachtwoord opslaan)
  • JWT Tokens — Korte levensduur (1u access, 7d refresh)
  • Row Level Security — Op alle tabellen ingeschakeld
  • Cirkel RLS — Alleen leden kunnen inhoud zien
  • Notitie RLS — Schrijver → metadata, Ontvanger → inhoud NA opening
  • Mentor-only — Uitnodigen, datum wijzigen, cirkel verwijderen

Testen:

# 1. Zorg dat RLS ingeschakeld is
SELECT tablename FROM pg_tables
WHERE schemaname='public';

# 2. Check policies bestaan
SELECT * FROM pg_policies;

# 3. Test RLS
-- Login als user A
SELECT * FROM cirkels;  -- Moet empty zijn (geen lid)

-- Voeg user A toe als lid
INSERT INTO lidmaatschappen VALUES (...);

SELECT * FROM cirkels;  -- Moet cirkel tonen

Encryptie

  • HTTPS Everywhere — Vercel + Supabase (TLS 1.3)
  • Notities AES-256-GCM — Client-side encryptie
  • Cirkel-sleutel — Opgeslagen in Supabase Vault (encrypted at rest)
  • IV Uniek — Elke notitie heeft random IV (12 bytes)
  • Server Ziet Nooit Plaintext — Alleen ciphertext + IV
  • Sleutel Timing — RLS blokkeert sleutel tot opening_datum

Testen:

// 1. Encryptie-decryptie roundtrip
const tekst = "Geheim";
const sleutel = await genereerCirkelSleutel();
const { versleuteld, iv } = await versleutelNotitie(tekst, sleutel);
const decrypted = await ontsleutelNotitie(versleuteld, iv, sleutel);
console.assert(decrypted === tekst, "FAIL: Encryptie niet werkend");

// 2. Server kan niet decrypten
// (dump DB: versleutelde_tekst mag niet leesbaar zijn)

// 3. RLS blokkeert sleutel tot opening
SELECT cirkel_sleutel FROM cirkels WHERE opening_datum > NOW();
// Moet NULL zijn

Privacy & GDPR/AVG

  • Expliciete Opt-In WhatsApp — Apart checkbox (niet standaard aan)
  • Privacyverklaring — Vereist bij registratie
  • Data Minimalisatie — Alleen e-mail, naam, 06 opgeslagen
  • Recht op Verwijdering — Zelfservice + automatisch na 30d
  • Audit Trail Geanonimiseerd — Geen email opgeslagen, alleen hash
  • Geen Tracking Pixels — Emails zijn plain-text compatible
  • Nederlandse Verwerkersovereenkomst — Supabase EU-only

Testen:

# 1. Audit log niet identificeerbaar
SELECT * FROM verwijderlogs
WHERE actie = 'account_vernietigd';
-- email_hash moet onleesbare hash zijn (SHA-256)
-- voornaam_initials mag niet full name bevatten

# 2. Automatische verwijdering na 30d
SELECT COUNT(*) FROM profielen
WHERE verwijder_op < NOW() AND verwijder_op IS NOT NULL;
-- Moet 0 zijn (cron moet hebben gerund)

# 3. Privacy policy zichtbaar
curl https://liefdevolleblik.nl/privacy
# Moet volledig privacybeleid tonen

Rate Limiting & Misbruik

  • API Rate Limiting — 10 req/min per IP (Vercel middleware)
  • Max Leden per Cirkel — 50 (database constraint)
  • Max 1 Notitie per Schrijver per Ontvanger — (unique constraint)
  • Tokens Éénmalig — Uitnodigingstokens kunnen maar 1x gebruikt worden
  • Token Expiry — 7 dagen (auto-verwijderd)
  • CAPTCHA op Signup — hCaptcha (niet Google reCAPTCHA)

Testen:

# 1. Rate limiting
for i in {1..15}; do
  curl -H "X-Forwarded-For: 1.2.3.4" https://liefdevolleblik.nl/api/...
done
# Na 10 requests: 429 Too Many Requests

# 2. hCaptcha aanwezig
curl https://liefdevolleblik.nl/registreer
# HTML moet hCaptcha script bevatten

# 3. Token éénmalig
# Gebruik invitation token 2x
# 1e keer: succesvol
# 2e keer: 400 Bad Request

Infrastructure & Secrets

  • Environment Variables — Nooit in code (.env in .gitignore)
  • Service Role Key — Alleen in server-side cron (Edge Functions)
  • Vault Encrypted Keys — Cirkel-sleutel encrypted at rest
  • CSP Headers — Content-Security-Policy ingesteld
  • CORS — Alleen liefdevolleblik.nl allowed
  • Vercel Secrets — Edge Functions haben aparte secrets

Testen:

# 1. Secrets niet in git
git log -p --all --grep="SUPABASE_KEY"
# Moet niks returnen

git log -S "eyJ" --oneline
# Moet niks returnen (base64 JWT)

# 2. CSP headers aanwezig
curl -I https://liefdevolleblik.nl
# Content-Security-Policy: default-src 'self' ...

# 3. CORS check
curl -H "Origin: evil.com" \
     -H "Access-Control-Request-Method: POST" \
     https://liefdevolleblik.nl/api/notitie
# Mag GEEN Access-Control-Allow-Origin teruggeven

Logging & Monitoring

  • Geen Notitie-Inhoud Gelogd — (Vervel moet dit checken)
  • Error Tracking — Sentry of equivalent
  • Performance Monitoring — Datadog / Cloudflare Analytics
  • Audit Logging — Alle data-vernietiging gelogd
  • Alert op Fout — Cron jobs sturen alert bij failure

Testen:

# 1. Logs bevatten geen notitie-tekst
grep -r "notitie" /path/to/logs
# Mag niet "Geheim briefje" bevatten

# 2. Sentry verbonden
curl https://liefdevolleblik.nl/api/error-test
# Error moet in Sentry verschijnen

# 3. Cron logs beschikbaar
supabase functions logs dagelijkse-check
# Moet logs tonen (success of error)

Database Security

  • RLS Policies — Op alle tabellen
  • Foreign Keys — CASCADE delete correct
  • Constraints — Telefoon-format check, unique emails
  • Indexes — Performance optimizations
  • Encryption at Rest — Supabase default
  • Backups — Automatisch (Supabase)

Testen:

-- 1. RLS ingeschakeld
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname='public';
-- Alle moeten 't' zijn voor rowsecurity

-- 2. Constraints
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_name='profielen';

-- 3. Backups
-- Supabase Dashboard → Database → Backups
-- Moet auto-backups tonen

API Security

  • Input Validation — Zod schemas op alle routes
  • SQL Injection Prevention — Parameterized queries (Supabase)
  • XSS Prevention — React auto-escapes, no innerHTML
  • CSRF Protection — SameSite=Strict cookies
  • Middleware Auth — Protected routes checks token

Testen:

// 1. Input validation
const response = await fetch('/api/cirkel', {
  method: 'POST',
  body: JSON.stringify({
    naam: '"; DROP TABLE cirkels; --',
    opening_datum: 'invalid'
  })
});
// Moet 400 Bad Request (Zod validation)

// 2. XSS
const response = await fetch('/api/cirkel', {
  method: 'POST',
  body: JSON.stringify({
    naam: '<script>alert("xss")</script>',
    opening_datum: '2025-12-31'
  })
});
// Naam wordt escaped in HTML

Third-Party Security

  • WhatsApp API — Valideer messages, rate limit
  • Resend Email — DKIM/SPF/DMARC records
  • hCaptcha — Verify token server-side
  • Supabase Auth — Magic link validation

Testen:

# 1. DKIM/SPF check
mxtoolbox.com
# liefdevolleblik.nl moet DKIM/SPF pass

# 2. Email deliverability
# Send test email from Resend
# Check spam folder (moet in inbox zijn)

# 3. hCaptcha verification
# Signup met hCaptcha
# Check backend log: verification success

Pre-Launch Checklist (48 uur voor launch)

  • Alle secrets in Vercel (niet in git)
  • RLS policies tested
  • Encryption tested (roundtrip)
  • Rate limiting tested
  • CAPTCHA working
  • WhatsApp messages tested
  • Email templates tested
  • Cron jobs scheduled
  • Database backups enabled
  • Error tracking enabled
  • Security headers enabled
  • HTTPS working
  • Privacy policy live
  • Terms of Service live (if required)

Regular Security Audits

Maandelijks

  • Check Supabase security advisories
  • Review audit logs
  • Check failed auth attempts
  • Verify backups are recent

Kwartaals

  • Dependency updates (npm audit)
  • Penetration testing (if budget)
  • Data breach simulation

Jaarlijks

  • Full security audit
  • GDPR compliance review
  • Disaster recovery drill

Incident Response

Wat te doen bij security incident:

  1. Discover: Bug report → confirm exploit
  2. Contain: Disable affected feature (if critical)
  3. Investigate: Check logs, database
  4. Fix: Patch code, update database
  5. Communicate: Email users (transparency)
  6. Post-Mortem: Learn & document

Incident Contacts:


Vragen over security? Open issue op GitHub met label security.