26 KiB
Security Remediation Plan - Mini-EPD Prototype
Versie: 1.0 Datum: 22 november 2025 Status: In Progress Eigenaar: Development Team
Inhoudsopgave
- Executive Summary
- Critical Issues (Week 1)
- High Priority Issues (Week 1-2)
- Medium Priority Issues (Week 2-3)
- Long-term Improvements (Week 4+)
- Verification & Testing
- Compliance Checklist
Executive Summary
Huidige Status
- Security Score: 4/10
- Critical Issues: 1
- High Priority: 3
- Medium/Low Priority: 7
- GDPR Compliant: ❌ Nee
Doelstelling
- Target Security Score: 9/10
- Target Completion: 4 weken
- GDPR Compliance: ✅ Ja (binnen 6 weken)
Impact
- Development Time: ~60-80 uur
- Risk Reduction: 85%
- Compliance: AVG/GDPR ready
Critical Issues (Week 1)
🔴 Issue #1: Exposed Production Credentials
Severity: CRITICAL Priority: P0 (Immediate) Effort: 2 uur Status: ⏳ Not Started
Probleem
.env.example bevat echte productie API keys in plaats van placeholders.
Bestand: .env.example:4-5,14
# HUIDIGE SITUATIE (GEVAARLIJK!)
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci... # Echte key!
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... # Echte key!
ANTHROPIC_API_KEY=sk-ant-api03-rrPx... # Echte key!
Impact
- ⚠️ Service role key geeft volledige database toegang
- ⚠️ Iedereen met repository toegang kan alle data lezen/schrijven
- ⚠️ API misbruik mogelijk
- ⚠️ GDPR data breach
Remediation Steps
Stap 1: Roteer alle API keys (URGENT)
# 1. Supabase Dashboard
# → Settings → API → Project API keys
# → "Reset anon key" en "Reset service_role key"
# 2. Anthropic Dashboard
# → Settings → API Keys → "Delete" oude key → "Create new key"
# 3. Update .env.local met nieuwe keys
Stap 2: Update .env.example
# .env.example - ALLEEN PLACEHOLDERS!
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
ANTHROPIC_API_KEY=sk-ant-your-key-here
Stap 3: Verificatie
# Check git history voor gelekte keys
git log -p -- .env.example | grep -E "eyJhbGci|sk-ant"
# Als keys gevonden: consider repository als compromised
# Overwegen: nieuwe private fork maken
Acceptance Criteria
- .env.example bevat alleen placeholders
- Alle productie keys zijn geroteerd
- Git history gecontroleerd op leaks
- Team geïnformeerd over nieuwe keys
- .env.local is toegevoegd aan .gitignore (already done ✅)
Verificatie
# Test dat app nog werkt met nieuwe keys
pnpm dev
# Navigeer naar http://localhost:3000 en test login
High Priority Issues (Week 1-2)
🟠 Issue #2: Next.js Vulnerabilities
Severity: HIGH Priority: P1 Effort: 1 uur Status: ⏳ Not Started
Probleem
Next.js 14.2.18 heeft 7+ bekende security vulnerabilities:
- CVE-2024-56332: DoS via Server Actions
- CVE-2025-48068: Source code exposure
- 5+ additional issues
Bestand: package.json:25
Remediation Steps
# Update Next.js naar latest patched versie
pnpm update next@latest
# Of specifiek naar 14.2.30+
pnpm add next@14.2.30
# Test build
pnpm build
# Run audit
pnpm audit
Acceptance Criteria
- Next.js >= 14.2.30
pnpm audittoont 0 high/critical issues- Build succesvol
- Alle functionaliteit werkt nog
🟠 Issue #3: Ontbrekende Security Headers
Severity: HIGH Priority: P1 Effort: 3 uur Status: ⏳ Not Started
Probleem
Kritieke security headers ontbreken, waardoor XSS, clickjacking en andere attacks mogelijk zijn.
Bestand: next.config.mjs
Remediation Steps
Stap 1: Voeg security headers toe
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
// ... existing config ...
async headers() {
return [
{
// Apply headers to all routes
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()'
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'", // Next.js needs unsafe-eval
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://dqugbrpwtisgyxscpefg.supabase.co",
"frame-ancestors 'self'"
].join('; ')
}
]
}
]
}
}
export default nextConfig;
Stap 2: Test headers
# Start dev server
pnpm dev
# Test in browser console
curl -I http://localhost:3000 | grep -E "X-Frame|Content-Security"
# Of gebruik online tool: https://securityheaders.com/
Acceptance Criteria
- Alle security headers aanwezig
- CSP policy implementeerd (zonder app te breken)
- Test op securityheaders.com score A of B
- Geen console errors door CSP
Verificatie Script
#!/bin/bash
# test-security-headers.sh
URL="http://localhost:3000"
echo "Testing security headers..."
HEADERS=(
"X-Frame-Options"
"X-Content-Type-Options"
"Strict-Transport-Security"
"Content-Security-Policy"
"Referrer-Policy"
)
for HEADER in "${HEADERS[@]}"; do
VALUE=$(curl -s -I "$URL" | grep -i "$HEADER")
if [ -n "$VALUE" ]; then
echo "✅ $VALUE"
else
echo "❌ Missing: $HEADER"
fi
done
🟠 Issue #4: Te Permissieve CORS Policy
Severity: HIGH Priority: P1 Effort: 2 uur Status: ⏳ Not Started
Probleem
Access-Control-Allow-Origin: * staat CSRF aanvallen toe.
Bestand: app/api/leads/route.ts:110
Remediation Steps
Stap 1: Update CORS policy
// app/api/leads/route.ts
// Add at top of file
const ALLOWED_ORIGINS = [
'https://yourdomain.com',
'https://www.yourdomain.com',
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : [])
];
export async function OPTIONS(request: NextRequest) {
const origin = request.headers.get('origin');
const allowedOrigin = ALLOWED_ORIGINS.includes(origin || '')
? origin
: ALLOWED_ORIGINS[0];
return NextResponse.json({}, {
status: 200,
headers: {
'Access-Control-Allow-Origin': allowedOrigin || '',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400', // 24 hours
}
});
}
export async function POST(request: NextRequest) {
// Add origin check
const origin = request.headers.get('origin');
if (origin && !ALLOWED_ORIGINS.includes(origin)) {
return NextResponse.json(
{ error: 'Origin not allowed' },
{ status: 403 }
);
}
// ... rest of existing code ...
}
Stap 2: Test CORS
# Test valid origin
curl -X POST http://localhost:3000/api/leads \
-H "Origin: http://localhost:3000" \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","projectType":"web","message":"Test message test message test"}'
# Test invalid origin (should fail)
curl -X POST http://localhost:3000/api/leads \
-H "Origin: https://evil.com" \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","projectType":"web","message":"Test message test message test"}'
Acceptance Criteria
- CORS beperkt tot specifieke origins
- Development localhost werkt nog
- Invalid origins worden geweigerd
- OPTIONS preflight werkt correct
🟠 Issue #5: FHIR API zonder Authenticatie
Severity: HIGH Priority: P1 Effort: 2 uur Status: ⏳ Not Started
Probleem
FHIR endpoints zijn publiek toegankelijk zonder authenticatie check.
Bestanden:
app/api/fhir/Patient/route.tsapp/api/fhir/Patient/[id]/route.tsapp/api/fhir/Practitioner/route.tsapp/api/fhir/Practitioner/[id]/route.ts
Remediation Steps
Stap 1: Voeg auth check toe aan alle FHIR endpoints
// app/api/fhir/Patient/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/auth/server';
import { supabaseAdmin } from '@/lib/supabase/server';
// ... other imports ...
export async function GET(request: NextRequest) {
try {
// ✅ ADD: Verify authentication
const supabase = await createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json(
createOperationOutcome('error', 'security', 'Authentication required'),
{ status: 401 }
);
}
// Rest of existing code...
const { searchParams } = new URL(request.url);
// ...
} catch (error) {
// ...
}
}
export async function POST(request: NextRequest) {
try {
// ✅ ADD: Verify authentication
const supabase = await createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json(
createOperationOutcome('error', 'security', 'Authentication required'),
{ status: 401 }
);
}
// Rest of existing code...
} catch (error) {
// ...
}
}
Stap 2: Herhaal voor alle FHIR endpoints
Pas dezelfde pattern toe op:
app/api/fhir/Patient/[id]/route.tsapp/api/fhir/Practitioner/route.tsapp/api/fhir/Practitioner/[id]/route.ts
Stap 3: Test authenticatie
# Test zonder auth (should fail)
curl http://localhost:3000/api/fhir/Patient
# Test met auth (should succeed)
# 1. Login via browser en copy session cookie
# 2. Test met cookie:
curl http://localhost:3000/api/fhir/Patient \
-H "Cookie: sb-access-token=YOUR_TOKEN_HERE"
Acceptance Criteria
- Alle FHIR endpoints vereisen authenticatie
- Unauthenticated requests krijgen 401
- Authenticated requests werken normaal
- Error messages zijn niet te verbose (geen stack traces)
Medium Priority Issues (Week 2-3)
🟡 Issue #6: Te Permissieve RLS Policies
Severity: MEDIUM Priority: P2 Effort: 8 uur Status: ⏳ Not Started
Probleem
Alle authenticated users kunnen ALLE data lezen/schrijven/verwijderen. Geen multi-tenancy isolatie.
Bestand: supabase/migrations/20241115000002_create_epd_core_tables.sql:168-181
Huidige Situatie
-- TOO PERMISSIVE!
CREATE POLICY "Allow all for authenticated users" ON clients
FOR ALL USING (auth.uid() IS NOT NULL);
Remediation Plan
Optie A: User-based isolation (Simple)
-- Optie A: Elke user ziet alleen eigen data
-- 1. Voeg user_id toe aan tabellen
ALTER TABLE clients ADD COLUMN created_by UUID REFERENCES auth.users(id);
-- 2. Update policies
DROP POLICY "Allow all for authenticated users" ON clients;
CREATE POLICY "Users can view own clients" ON clients
FOR SELECT USING (created_by = auth.uid());
CREATE POLICY "Users can insert own clients" ON clients
FOR INSERT WITH CHECK (created_by = auth.uid());
CREATE POLICY "Users can update own clients" ON clients
FOR UPDATE USING (created_by = auth.uid());
CREATE POLICY "Users can delete own clients" ON clients
FOR DELETE USING (created_by = auth.uid());
Optie B: Organization-based isolation (Recommended)
-- Optie B: Multi-tenancy met organizations
-- 1. Voeg org_id toe aan tabellen
ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
-- 2. Voeg user_organizations mapping toe
CREATE TABLE user_organizations (
user_id UUID REFERENCES auth.users(id),
org_id UUID REFERENCES organizations(id),
role TEXT CHECK (role IN ('admin', 'practitioner', 'readonly')),
PRIMARY KEY (user_id, org_id)
);
-- 3. Create helper function
CREATE OR REPLACE FUNCTION get_user_org_id(user_uuid UUID)
RETURNS UUID AS $$
SELECT org_id FROM user_organizations
WHERE user_id = user_uuid
LIMIT 1;
$$ LANGUAGE sql SECURITY DEFINER;
-- 4. Update policies
DROP POLICY "Allow all for authenticated users" ON clients;
CREATE POLICY "Users can view org clients" ON clients
FOR SELECT USING (
org_id = get_user_org_id(auth.uid())
);
CREATE POLICY "Users can insert org clients" ON clients
FOR INSERT WITH CHECK (
org_id = get_user_org_id(auth.uid())
);
-- Etc...
Migration File
Maak nieuwe migration: supabase/migrations/YYYYMMDD_implement_rls_isolation.sql
-- Migration: Implement proper RLS isolation
-- Option: Organization-based (recommended)
BEGIN;
-- Step 1: Add org_id to all tables
ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
ALTER TABLE intake_notes ADD COLUMN org_id UUID;
ALTER TABLE problem_profiles ADD COLUMN org_id UUID;
ALTER TABLE treatment_plans ADD COLUMN org_id UUID;
ALTER TABLE ai_events ADD COLUMN org_id UUID;
-- Step 2: Create user_organizations mapping
CREATE TABLE user_organizations (
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('admin', 'practitioner', 'readonly')),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_id, org_id)
);
-- Step 3: Helper function
CREATE OR REPLACE FUNCTION get_user_org_id(user_uuid UUID)
RETURNS UUID AS $$
SELECT org_id FROM user_organizations
WHERE user_id = user_uuid
LIMIT 1;
$$ LANGUAGE sql SECURITY DEFINER STABLE;
-- Step 4: Update RLS policies
-- CLIENTS
DROP POLICY IF EXISTS "Allow all for authenticated users" ON clients;
CREATE POLICY "org_select" ON clients FOR SELECT
USING (org_id = get_user_org_id(auth.uid()));
CREATE POLICY "org_insert" ON clients FOR INSERT
WITH CHECK (org_id = get_user_org_id(auth.uid()));
CREATE POLICY "org_update" ON clients FOR UPDATE
USING (org_id = get_user_org_id(auth.uid()));
CREATE POLICY "org_delete" ON clients FOR DELETE
USING (org_id = get_user_org_id(auth.uid()));
-- TODO: Repeat for other tables
COMMIT;
Acceptance Criteria
- Migration geschreven en getest
- Elke tabel heeft org_id kolom
- RLS policies filteren op org_id
- Users kunnen alleen eigen org data zien
- Test met 2+ orgs en 2+ users per org
Testing
-- Test script
-- 1. Create 2 orgs
INSERT INTO organizations (name) VALUES ('Org A'), ('Org B');
-- 2. Create 2 users (via Supabase auth)
-- 3. Map users to orgs
INSERT INTO user_organizations (user_id, org_id, role) VALUES
('user-1-uuid', 'org-a-uuid', 'practitioner'),
('user-2-uuid', 'org-b-uuid', 'practitioner');
-- 4. Create clients
INSERT INTO clients (first_name, last_name, birth_date, org_id) VALUES
('Client A', 'Org A', '1990-01-01', 'org-a-uuid'),
('Client B', 'Org B', '1990-01-01', 'org-b-uuid');
-- 5. Login as user-1 and query
-- Should only see Client A
-- 6. Login as user-2 and query
-- Should only see Client B
🟡 Issue #7: Organizations Table zonder Policies
Severity: MEDIUM Priority: P2 Effort: 1 uur Status: ⏳ Not Started
Probleem
Organizations tabel heeft RLS enabled maar geen policies.
Remediation Steps
-- supabase/migrations/YYYYMMDD_add_organizations_policies.sql
-- Allow authenticated users to view organizations they belong to
CREATE POLICY "users_view_own_orgs" ON organizations
FOR SELECT USING (
id IN (
SELECT org_id FROM user_organizations
WHERE user_id = auth.uid()
)
);
-- Allow org admins to update their organization
CREATE POLICY "admins_update_org" ON organizations
FOR UPDATE USING (
id IN (
SELECT org_id FROM user_organizations
WHERE user_id = auth.uid() AND role = 'admin'
)
);
-- Only service role can INSERT/DELETE organizations
-- (via migrations or admin panel)
Acceptance Criteria
- SELECT policy voor organizations
- UPDATE policy voor admins
- Users kunnen eigen org zien
- Users kunnen geen andere orgs zien
🟡 Issue #8: Rate Limiting
Severity: MEDIUM Priority: P2 Effort: 4 uur Status: ⏳ Not Started
Probleem
Geen rate limiting op API endpoints = spam/DoS risico.
Remediation Options
Optie A: Vercel KV + Upstash (Recommended voor Vercel deployment)
# Install
pnpm add @upstash/ratelimit @upstash/redis
// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
analytics: true,
});
// Usage in API route:
import { ratelimit } from '@/lib/rate-limit';
export async function POST(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
// ... rest of code
}
Optie B: Simple in-memory (Development only)
// lib/simple-rate-limit.ts
const requests = new Map<string, number[]>();
export function checkRateLimit(
ip: string,
maxRequests: number = 10,
windowMs: number = 10000
): boolean {
const now = Date.now();
const userRequests = requests.get(ip) || [];
// Filter out old requests
const recentRequests = userRequests.filter(
time => now - time < windowMs
);
if (recentRequests.length >= maxRequests) {
return false;
}
recentRequests.push(now);
requests.set(ip, recentRequests);
return true;
}
Endpoints die rate limiting nodig hebben
/api/leads- 10 req/10s per IP/api/fhir/PatientPOST - 20 req/min per user- Auth callbacks - 5 req/min per IP
🟡 Issue #9: CSRF Protection
Severity: MEDIUM Priority: P3 Effort: 2 uur Status: ⏳ Not Started
Current Status
Next.js gebruikt SameSite cookies (default = Lax), wat al bescherming biedt.
Additional Hardening
// lib/csrf.ts
import { NextRequest } from 'next/server';
export function verifyCsrfToken(request: NextRequest): boolean {
// Check Origin header matches host
const origin = request.headers.get('origin');
const host = request.headers.get('host');
if (origin && !origin.endsWith(host || '')) {
return false;
}
// Check Referer for additional validation
const referer = request.headers.get('referer');
if (referer && !referer.includes(host || '')) {
return false;
}
return true;
}
// Usage:
export async function POST(request: NextRequest) {
if (!verifyCsrfToken(request)) {
return NextResponse.json(
{ error: 'Invalid request origin' },
{ status: 403 }
);
}
// ...
}
Acceptance Criteria
- CSRF checks op alle POST/PUT/DELETE endpoints
- Origin header validation
- Tests voor cross-origin requests
🟡 Issue #10: Function Search Path
Severity: LOW Priority: P3 Effort: 1 uur Status: ⏳ Not Started
Probleem
3 database functies hebben mutable search_path.
Remediation
-- Fix all functions
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
CREATE OR REPLACE FUNCTION is_demo_user(user_uuid UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM demo_users WHERE user_id = user_uuid
);
$$ LANGUAGE sql
SECURITY DEFINER
STABLE
SET search_path = public, pg_temp;
CREATE OR REPLACE FUNCTION get_demo_access_level(user_uuid UUID)
RETURNS TEXT AS $$
SELECT access_level FROM demo_users WHERE user_id = user_uuid;
$$ LANGUAGE sql
SECURITY DEFINER
STABLE
SET search_path = public, pg_temp;
🟡 Issue #11: Leaked Password Protection
Severity: LOW Priority: P3 Effort: 0.5 uur (configuratie) Status: ⏳ Not Started
Remediation Steps
- Ga naar Supabase Dashboard
- Authentication → Providers → Email
- Scroll naar "Password Protection"
- Enable "Check for leaked passwords"
- Save
Link: https://supabase.com/docs/guides/auth/password-security
Long-term Improvements (Week 4+)
🔵 Audit Logging
Priority: P4 Effort: 12 uur
Implementatie
-- Create audit log table
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
action TEXT NOT NULL, -- 'SELECT', 'INSERT', 'UPDATE', 'DELETE'
table_name TEXT NOT NULL,
record_id UUID,
old_data JSONB,
new_data JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create trigger function
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (
user_id, action, table_name, record_id, old_data, new_data
) VALUES (
auth.uid(),
TG_OP,
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Apply to sensitive tables
CREATE TRIGGER audit_clients
AFTER INSERT OR UPDATE OR DELETE ON clients
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
🔵 Encryption at Rest
Status: ✅ Enabled by default (Supabase)
Supabase gebruikt AES-256 encryption voor alle data. Geen actie nodig.
🔵 Backup & Recovery
-- Automated backups (Supabase Pro)
-- Daily point-in-time recovery (7 days retention)
-- Manual backup script
-- backup.sh
#!/bin/bash
DATE=$(date +%Y%m%d)
pg_dump $DATABASE_URL > backups/backup-$DATE.sql
Verification & Testing
Security Testing Checklist
Authentication Tests
- Unauthenticated users kunnen geen protected routes benaderen
- Session cookies zijn HttpOnly en Secure
- Session expiry werkt correct
- Password reset flow is secure
Authorization Tests
- Users kunnen alleen eigen org data zien
- Users kunnen geen andere org data wijzigen
- Admin privileges worden correct gehandhaafd
- Service role bypassed RLS (expected)
API Security Tests
# Test suite
./scripts/security-tests.sh
# Tests:
# 1. CORS - invalid origin rejected
# 2. Rate limiting - 429 after limit
# 3. Auth - 401 without token
# 4. CSRF - cross-origin POST rejected
# 5. Headers - all security headers present
OWASP Top 10 Checklist
- A01:2021 – Broken Access Control → RLS policies
- A02:2021 – Cryptographic Failures → HTTPS + Supabase encryption
- A03:2021 – Injection → Supabase ORM (no raw SQL)
- A04:2021 – Insecure Design → Security-first architecture
- A05:2021 – Security Misconfiguration → Security headers
- A06:2021 – Vulnerable Components → Dependency updates
- A07:2021 – Identification/Authentication Failures → Supabase Auth
- A08:2021 – Software/Data Integrity Failures → Code signing (TODO)
- A09:2021 – Security Logging Failures → Audit log (TODO)
- A10:2021 – Server-Side Request Forgery → No SSRF vectors
Compliance Checklist
AVG/GDPR Requirements
Data Protection
- Data minimalisatie principe toegepast
- Encryptie at rest (Supabase ✅)
- Encryptie in transit (HTTPS ✅)
- Toegangscontrole (RLS policies)
- Audit logging geïmplementeerd
User Rights
- Recht op inzage (export functie)
- Recht op rectificatie (edit functie)
- Recht op vergetelheid (delete with cascade)
- Recht op dataportabiliteit (FHIR export)
- Recht op bezwaar (opt-out mechanisme)
Legal Basis
- Privacy policy aanwezig
- Cookie consent (indien tracking)
- Data Processing Agreement met Supabase
- Data breach notification procedure
Documentation
- Data mapping (welke data wordt opgeslagen)
- Retention policy (hoe lang bewaren)
- Access logs (wie heeft toegang gehad)
- Security incident response plan
Progress Tracking
Week 1
- Issue #1: Rotate credentials ⚠️ URGENT
- Issue #2: Update Next.js
- Issue #3: Security headers
- Issue #4: CORS policy
- Issue #5: FHIR auth
Week 2
- Issue #6: RLS policies
- Issue #7: Organizations policies
- Issue #8: Rate limiting
Week 3
- Issue #9: CSRF protection
- Issue #10: Function search path
- Issue #11: Password protection
- Security testing
Week 4
- Audit logging
- Documentation
- Compliance review
- Final security scan
Resources
Tools
Documentation
Last Updated: 22 november 2025 Next Review: Na afronding Week 1 (29 november 2025)