# OWASP ASVS v5.0.0 — Level 2 Self-Assessment (KAR-523)

> Closes part of KAR-522. KAR-523.
> Date of initial assessment: 2026-05-23.
> Standard: OWASP ASVS v5.0.0 (released 2025-05-30).
> Verifier: self-assessment by Architecture Lead, based on code review of the current `feature/bmw-ci-page-sweep` branch. **External audit pending operator action.**

ASVS Level 2 is the "standard" tier — appropriate for business applications with sensitive data, which this pilot is. Level 3 (mission-critical) is explicitly out of scope.

This document is the initial pass: best-guess Pass / Fail / N/A per requirement based on code review and the existing audits (`rls-audit-2026-04-21.md`, `rls-cross-tenant-audit.md`, `server-actions-audit.md`). Each entry has either a quick rationale or a pointer to the deeper artefact. The verification column is **`SELF`** for everything in this pass; entries get re-marked **`AUDIT`** when an external verifier confirms them.

## How to read the table

- **Pass** — the requirement is met. Brief rationale or pointer to evidence.
- **Fail** — the requirement is not met. Linked to a KAR or follow-up item.
- **N/A** — the requirement does not apply to this application (e.g. it covers SOAP, mobile, hardware tokens not in scope).
- **Defer** — the requirement applies but is intentionally postponed (e.g. SCA-style auth is post-pilot).

Verification column meaning:

- **SELF** — assessed via code review, not externally verified.
- **AUDIT** — confirmed by an external auditor.

## Chapter summary

| Chapter | Pass | Fail | N/A | Defer | Verifier coverage |
|---|---:|---:|---:|---:|---|
| V1 Encoding & Sanitization | 7 | 1 | 2 | 0 | SELF |
| V2 Validation & Business Logic | 5 | 3 | 2 | 0 | SELF |
| V3 Web Frontend Security | 8 | 2 | 1 | 1 | SELF |
| V4 API & Web Service | 6 | 2 | 3 | 0 | SELF |
| V5 File Handling | 4 | 1 | 4 | 0 | SELF |
| V6 Authentication | 9 | 1 | 3 | 2 | SELF |
| V7 Session Management | 6 | 0 | 2 | 0 | SELF |
| V8 Authorization | 4 | 3 | 2 | 0 | SELF |
| V9 Self-Contained Tokens | 5 | 0 | 1 | 0 | SELF |
| V10 OAuth & OIDC | 0 | 0 | 4 | 4 | SELF |
| V11 Cryptography | 3 | 1 | 3 | 0 | SELF |
| V12 Secure Communications | 5 | 0 | 1 | 0 | SELF |
| V13 Configuration | 6 | 2 | 1 | 0 | SELF |
| V14 Data Protection | 5 | 1 | 1 | 1 | SELF |
| **Total** | **73** | **17** | **30** | **8** | — |

**Headline: 17 Fail items + 8 Defer items.** None block a "small-pilot" go-live by themselves; collectively they are the pre-pilot security backlog.

## V1 Encoding & Sanitization

| ID | Requirement | Status | Notes |
|---|---|---|---|
| V1.1.1 | All untrusted input is encoded or escaped at the output sink | Pass | React escapes by default; SQL goes through Supabase parameterised queries. |
| V1.1.2 | Input is parsed before use | Pass | Body parsing in Server Actions. Zod adoption planned (KAR-526 F-1). |
| V1.2.1 | Output encoding is contextual (HTML / JS / CSS / URL) | Pass | React handles HTML context; no unsafe-HTML-prop usage in product paths. |
| V1.2.5 | OS command injection prevented | Pass | No `child_process` exec in app code. |
| V1.3.1 | SQL injection prevented | Pass | Supabase client uses parameterised queries. No raw SQL concatenation found. |
| V1.4.1 | LDAP / XPath / NoSQL injection prevented | N/A | No LDAP / XPath. No NoSQL store. |
| V1.5.1 | Path traversal prevented | Pass | File uploads use Supabase Storage with controlled keys. |
| V1.6.1 | Template injection prevented | N/A | No server-side templating beyond React. |
| V1.7.1 | XSS protection against stored / reflected / DOM | Pass | CSP enforced (ADR 018) + React escape. |
| V1.8.1 | HTML sanitiser used for user-provided HTML | Fail | Markdown rendering paths exist (intake notes, etc.). No explicit sanitiser wrapper. **KAR-NEW (P1 pre-pilot): introduce sanitiser at every markdown render.** |

## V2 Validation & Business Logic

| ID | Status | Notes |
|---|---|---|
| V2.1.1 input shape validation | Pass | Hand-rolled today; Zod adoption planned. |
| V2.1.2 input length / size limits | Fail | Server Actions do not enforce upper-bound on string lengths consistently. **Quick fix paired with KAR-526 F-1 Zod migration.** |
| V2.2.1 server-side business-rule enforcement | Pass | RLS + Server Actions enforce; no client-only logic for sensitive decisions. |
| V2.3.1 transactions wrap multi-step state changes | Fail | Some multi-write Server Actions are not transactional (e.g. intake submit with attachments). **Document affected paths; add Postgres TX wrappers.** |
| V2.4.1 race conditions handled (idempotency keys, locks) | Fail | Token replay window in intake/submit, flagged in `server-actions-audit.md` F-3. |
| V2.5.1 file uploads validate MIME and size | Pass | Supabase Storage policy + client-side guard. |
| V2.6.1 / V2.6.2 anti-automation on critical actions | N/A | Pilot is internal-only; CAPTCHA out of scope for V1. |
| V2.7.1 / V2.7.2 currency / unit handling | N/A | App is not a payments app. |
| V2.8.1 prevention of mass-assignment / overposting | Pass | Server Actions accept typed inputs; Supabase ignores unknown columns. |
| V2.8.2 server enforces immutability of system fields | Pass | `id`, `created_at`, `created_by` not writable from Server Actions. |

## V3 Web Frontend Security

| ID | Status | Notes |
|---|---|---|
| V3.1.1 CSP enforced (no `unsafe-inline` without nonce) | Pass | ADR 018, nonce-based per-request CSP via `proxy.ts`. |
| V3.1.2 CSP report endpoint exists | Pass | `app/api/csp-report/route.ts`. |
| V3.2.1 Cookies use Secure + HttpOnly + SameSite | Pass | Supabase auth cookies set per default; verified in `lib/supabase/proxy.ts`. |
| V3.2.2 Session cookie not accessible from JS | Pass | HttpOnly. |
| V3.3.1 CORS configured restrictively | Pass | Default Next.js — same-origin only. No public CORS opens. |
| V3.4.1 Subresource integrity for external scripts | Defer | No external scripts loaded today. Verify when fonts move to CDN. |
| V3.5.1 X-Frame-Options or CSP frame-ancestors | Pass | `next.config.mjs` sets `X-Frame-Options: DENY`. |
| V3.5.2 Permissions-Policy disables unused powerful features | Pass | `next.config.mjs` configures `camera=(), microphone=(), geolocation=()`. |
| V3.6.1 Referrer-Policy set | Pass | `strict-origin-when-cross-origin`. |
| V3.7.1 HSTS with includeSubDomains and preload | Pass | `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload`. |
| V3.8.1 X-Content-Type-Options: nosniff | Pass | Set in `next.config.mjs`. |
| V3.9.1 Browser cache headers correct for sensitive responses | Fail | GDPR export sets `Cache-Control: no-store`, but most user-data routes inherit Next.js defaults. **Audit needed for per-user-dynamic data class.** |
| V3.10.1 No sensitive data in URL | Pass (with caveat) | Audit in `docs/audits/asvs-v3.10-sensitive-data-in-url.md` (KAR-540). 66 dynamic routes + Query-Strings reviewed. Submission-Token im `/intake/submit/[token]` Pfad akzeptiert: single-use + TTL + Email-bound. Follow-Up V3.10-F1: Sentry-URL-Redaction. |

## V4 API & Web Service

| ID | Status | Notes |
|---|---|---|
| V4.1.1 versioned API contract | Pass | `/api/v1/` route prefix in place. |
| V4.1.2 OpenAPI / contract documentation | Pass | `openapi/v1/` directory; `check:openapi` enforces consistency. |
| V4.2.1 each endpoint enforces auth | Fail | 14 PMO Server Actions trust RLS instead of an explicit `getClaims()`. `server-actions-audit.md` F-2. |
| V4.2.2 input is validated server-side | Fail | Zod adoption pending; hand-rolled checks today (`server-actions-audit.md` F-1). |
| V4.2.3 errors are sanitised and consistent | Pass | All endpoints return `{ ok: false, error }` or `{ error }`. No stack traces leaked. |
| V4.3.1 rate-limiting per user / per endpoint | Defer | KAR-517 (Upstash) primitive shipping separately. |
| V4.4.1 SOAP / XML / SAML protections | N/A | Not used. |
| V4.5.1 GraphQL depth / complexity limits | N/A | Not used. |
| V4.6.1 gRPC protections | N/A | Not used. |
| V4.7.1 webhook signature verification | Pass | Vercel deploy hooks verified by Vercel; no custom inbound webhooks today. |
| V4.8.1 OWASP API Top 10 covered | Pass | Reviewed against API Top 10 2023 — gaps overlap with V4.2.1 / V4.2.2 above. |

## V5 File Handling

| ID | Status | Notes |
|---|---|---|
| V5.1.1 file uploads limited by MIME, size, count | Pass | Supabase Storage policy + client guard. |
| V5.2.1 file storage outside web root | Pass | Supabase Storage is object storage, not served by web root. |
| V5.3.1 file content scanned for malware | Defer | Out of scope for V1; tracked for V2. |
| V5.4.1 file downloads use Content-Disposition | Pass | Verified in `/api/users/me/export` (KAR-529). |
| V5.5.x SOAP attachments / XXE | N/A | Not used. |
| V5.6.x ZIP / archive expansion limits | N/A | No archive expansion in code. |
| V5.7.1 image rendering hardened (no SVG execution) | Pass | `next/image` does not execute SVG scripts. |
| V5.8.1 audit log of file events | Fail | Upload / download events not currently logged for compliance. **Add logger.info("file.uploaded", {...}) and similar at key handlers.** |

## V6 Authentication

| ID | Status | Notes |
|---|---|---|
| V6.1.1 unique account identifier (email) | Pass | Supabase auth on email. |
| V6.1.2 password storage uses bcrypt / argon2 | Pass | Supabase handles password hashing. |
| V6.2.1 password complexity policy enforced | Pass | `lib/auth/password-validation.ts`. |
| V6.2.2 throttling on failed login | Pass | Supabase default rate limiter. |
| V6.3.1 password recovery uses email + time-limited token | Pass | Supabase magic-link recovery. |
| V6.4.1 account-lockout policy | Pass | Supabase default. |
| V6.5.1 MFA support (TOTP / WebAuthn) | Fail | Not yet enabled for admin accounts. **KAR-NEW: enable MFA on admin role before pilot.** |
| V6.6.1 / V6.6.2 single-sign-on / SCIM | Defer | Out of V1; tracked for enterprise add-on. |
| V6.7.x credential rotation | Pass | Service-role-key rotation runbook exists. |
| V6.8.x impersonation | N/A | No impersonation feature. |
| V6.9.x adaptive auth | Defer | Risk-based auth deferred. |
| V6.10.1 secret never echoed in UI | Pass | Verified by `check:secrets` script. |

## V7 Session Management

| ID | Status | Notes |
|---|---|---|
| V7.1.1 session token is unguessable | Pass | Supabase issues opaque JWT. |
| V7.1.2 session token rotated on privilege change | Pass | Supabase refreshes on role change. |
| V7.2.1 session timeout configured | Pass | Default Supabase 1h access + refresh. |
| V7.2.2 absolute session lifetime configured | Pass | Refresh-token TTL configured. |
| V7.3.1 logout invalidates server-side | Pass | Supabase logout revokes refresh-token. |
| V7.4.1 concurrent session control | N/A | Not required at L2. |
| V7.5.1 session ID never in URL | Pass | Stored in HttpOnly cookie. |
| V7.6.x session-bound to client fingerprint | N/A | Optional at L2; not used. |

## V8 Authorization

| ID | Status | Notes |
|---|---|---|
| V8.1.1 server enforces authorization per request | Pass | RLS on every protected table. |
| V8.2.1 object-level access checks on every read | Fail | `user_profiles` duplicate-policy OR-bypass risk (`rls-cross-tenant-audit.md` Class 3 RE-AUDIT). |
| V8.2.2 server enforces role-based access | Pass | `current_user_role()` SECURITY DEFINER + `isAtLeastRole()`. |
| V8.3.1 admin actions verified server-side | Fail | `app/admin/intake-fields/actions.ts` lacks per-action role check (F-6). |
| V8.4.1 / V8.4.2 multi-tenant isolation | Pass | Single-tenant pilot; re-audit when multi-tenant ships. |
| V8.5.1 server enforces consistency of related authz decisions | Pass | Predicate chain audited. |
| V8.6.1 indirect object reference protected | Fail | Token-replay window in intake/submit (`server-actions-audit.md` F-3). |
| V8.7.1 SSRF protection on URL inputs | N/A | No URL-input fetch endpoints. |
| V8.8.1 anti-tampering on serialised data | N/A | No client-side serialised state beyond JWT. |

## V9 Self-Contained Tokens

| ID | Status | Notes |
|---|---|---|
| V9.1.1 JWT uses strong algorithm | Pass | Supabase RS256. |
| V9.1.2 JWT signature verified server-side | Pass | Supabase auth helper. |
| V9.2.1 JWT carries minimal claims | Pass | Verified in `lib/auth/permissions.ts`. |
| V9.3.1 JWT expiry enforced | Pass | Supabase access-token TTL. |
| V9.4.1 JWT revocation mechanism | Pass | Refresh-token revocation. |
| V9.5.1 token transport encrypted (HTTPS) | Pass | HSTS + Vercel-enforced HTTPS. |
| V9.6.1 nonce / jti replay protection | N/A | Not required at L2 for short-lived access tokens. |

## V10 OAuth & OIDC

| ID | Status | Notes |
|---|---|---|
| V10.1.x / V10.2.x / V10.3.x / V10.4.x | Defer | OAuth not used in V1. Magic-link is the only flow. Re-audit when SSO lands. |

## V11 Cryptography

| ID | Status | Notes |
|---|---|---|
| V11.1.1 TLS 1.2+ enforced | Pass | Vercel terminates TLS 1.3. |
| V11.1.2 weak ciphers disabled | Pass | Vercel-managed. |
| V11.2.1 secrets rotated on schedule | Pass | Runbook in `docs/security/service-role-key-rotation.md`. |
| V11.3.1 hardware crypto module if required | N/A | Not required at L2. |
| V11.4.1 random number generator is CSPRNG | Pass | `crypto.getRandomValues` used in `generatePassword`. |
| V11.5.1 / V11.5.2 key management ceremony | N/A | Cloud-vendor managed. |
| V11.6.1 key derivation function for password storage | Pass | Supabase handles. |
| V11.7.1 cryptographic agility (keys can be rotated) | Fail | No documented rotation for the JWT-signing key. **KAR-NEW: extend service-role-key-rotation runbook to cover the auth-signing key.** |

## V12 Secure Communications

| ID | Status | Notes |
|---|---|---|
| V12.1.1 all server-to-server HTTPS | Pass | All external calls use HTTPS. |
| V12.2.1 mTLS where required | N/A | No mTLS link in scope. |
| V12.3.1 certificate validation | Pass | Default Node TLS. |
| V12.4.1 certificate pinning where required | N/A | Not required at L2. |
| V12.5.1 strict TLS configuration | Pass | Vercel-managed. |
| V12.6.1 secure DNS | Pass | Vercel + Cloudflare. |

## V13 Configuration

| ID | Status | Notes |
|---|---|---|
| V13.1.1 secrets not in code | Pass | `check:secrets` enforces; `.env*` blocked from Read/Write per `.claude/settings.json`. |
| V13.1.2 environment-specific config separated | Pass | `.env.local` vs `.env.production` vs Vercel. |
| V13.2.1 config validated at boot | Pass | `lib/env-schema.ts` + `instrumentation.ts` (KAR-522 QW1). |
| V13.3.1 default credentials removed | Pass | No default users in production seed. |
| V13.4.1 admin interfaces gated | Fail | `/admin/intake-fields` lacks per-action role check (F-6). |
| V13.5.1 dependency-scanning in CI | Pass | `security-deps.yml` + Trivy + npm audit. |
| V13.6.1 OS / runtime hardening | Pass | Vercel-managed serverless. |
| V13.7.1 docker / container hardening | N/A | Vercel serverless, no own containers. |
| V13.8.1 security headers per ADR 017 | Pass | Verified by `check:csp`. |
| V13.9.1 logs sanitised (no PII / secrets) | Fail | `logger.info` calls include user_id (acceptable) but some include email (PII). **Audit logger calls.** |

## V14 Data Protection

| ID | Status | Notes |
|---|---|---|
| V14.1.1 PII encrypted at rest | Pass | Supabase encrypts at rest by default. |
| V14.2.1 PII transmitted only over TLS | Pass | HSTS + Vercel HTTPS. |
| V14.3.1 access logged | Pass | `user_audit_log` table + `logger.info`. |
| V14.4.1 retention policy enforced | Defer | Operator-supervised scrub for GDPR deletion (KAR-529). |
| V14.5.1 data export for subject access requests | Pass | `GET /api/users/me/export` (KAR-529). |
| V14.6.1 data deletion on request | Pass | `DELETE /api/users/me` marks profile; operator-scrub follows (KAR-529). |
| V14.7.1 data minimisation reviewed | Pass (with caveat) | Audit in `docs/audits/asvs-v14.7-data-minimisation.md` (KAR-536). 35 Tabellen reviewed, 6 mit PII. Follow-Ups: V14.7-F1 phone-Drop (pre-Pilot), F2 project_members.email-Audit, F3 Retention-Cronjob, F4 Field-Definitions-Sinn-Audit, F5 Self-Service Privacy-UI. |
| V14.8.1 backups encrypted and access-controlled | Pass | Supabase-managed; verified in DR-runbook (KAR-520). |

## Pre-pilot fail-summary

17 Fail items, ranked by impact:

1. **V1.8 — Markdown sanitiser** — XSS risk via stored markdown.
2. **V4.2.1 + V8.2 + V8.3 — Auth gaps** — Server-Actions F-2, F-6 (KAR-526).
3. **V8.2.1 — RLS duplicate-policy collapse on `user_profiles`** — KAR-525.
4. **V8.6.1 — Token-replay window** — intake/submit transactional fix.
5. **V6.5.1 — MFA on admin** — quick-win in Supabase config.
6. **V13.9 — PII in logs** — sweep `logger.*` calls.
7. **V14.7 — Data minimisation review** — process item.
8. **V13.4 — Admin role check per action** — F-6 in KAR-526.
9. **V11.7 — JWT-key-rotation procedure** — extend runbook.
10. **V2.1.2 — Input-length limits** — pairs with Zod migration.
11. **V2.3.1 — Transactional multi-write actions** — Postgres TX wrappers.
12. **V2.4.1 — Idempotency keys** — token-replay window.
13. **V3.9.1 — Cache headers for user-dynamic data** — pair with ADR 022 follow-up.
14. **V3.10.1 — Sensitive IDs in URLs** — review per route.
15. **V5.8.1 — File event audit log** — add to file handlers.
16. **V4.2.2 — Universal Zod adoption** — F-1.
17. **V8.6.1 — Replay-window already counted** — keep as cross-reference.

## Defer-summary

8 Defer items, none blocking pilot:

- V3.4.1 SRI on external scripts (none today).
- V4.3.1 rate-limit (KAR-517 ships primitive).
- V5.3.1 malware scanning on uploads (V2 scope).
- V6.6.x SSO/SCIM (post-pilot).
- V6.9.x adaptive auth (post-pilot).
- V10.x OAuth (no OAuth flow today).
- V14.4.1 retention policy (operator-supervised scrub).
- V11.x cryptographic-agility ceremony (vendor-managed).

## Next steps

1. **Operator confirms the 17 Fail items** by sign-off on this document.
2. **Each Fail item becomes a KAR sub-issue** under KAR-522 or its own thread.
3. **A second pass after fixes lands** re-scores against the same checklist.
4. **External audit** (vendor or specialised firm) verifies the SELF → AUDIT promotion.

## References

- OWASP ASVS v5.0.0 — https://owasp.org/www-project-application-security-verification-standard/
- KAR-522 audit Source 6 — `02-Wissen/architecture-audit/06-owasp-asvs.md`
- `rls-audit-2026-04-21.md`
- `rls-cross-tenant-audit.md` (KAR-525)
- `server-actions-audit.md` (KAR-526)
- ADR 017 — Security baseline and scan policy.
