A structured security assessment of the Feierabend (Decke 01) demo, framed against the CISSP domains and the OWASP Top 10. It records the audit method, what was found and fixed, the residual (accepted) risks, and what a formal certification would additionally require.
Scope & honesty note. This is a demo — no real customers, no payment data. "Certified secure" is not a state code can be in: certifications (SOC 2, ISO 27001) attest to an organization's operational controls over real data, not to a repository. What this document attests to is a defensible technical posture, verified by audit.
Three independent read-only audits over the edge functions
(supabase/functions/*), the database schema/RLS (supabase/setup.sql), and the
browser assets (admin.html, index.html, assets/*.js):
- Injection — every place request-derived input reaches a query (PostgREST filter strings, RPC args, raw URLs).
- XSS / DOM injection — every DOM sink that can execute markup/script, and whether the data reaching it is attacker-influenceable.
- Access control, RLS, and secrets — every HTTP entrypoint and its gate, RLS coverage on every table, and whether any secret can reach the browser.
Access control is solid. No critical/high access-control defect. One HIGH XSS found and fixed. No exploitable injection. All 24 tables have RLS with correct policies. No secret reaches the browser. Remaining items are low-severity or operational-process (certification) work.
| Sev | Finding | Fix |
|---|---|---|
| HIGH | Stored XSS in the storefront nav ticker: the customer-supplied city field (validated for length only) was rendered via innerHTML. A crafted city (<img src=x onerror=…>) would execute for every homepage visitor. |
Output-encode (escHtml) the DB-derived value before it enters the ticker markup, matching the safe textContent path the visible claims list already used. Defense-in-depth server-side city validation is tracked below. |
| MED | GET ?cachecheck=1 was unauthenticated but writes a probe row and runs an embedding — a write/compute-amplification surface. |
Admin-gated (requireAdmin). It has no client caller — it's a manual health check. |
| LOW | GET ?tools=1 (tool manifest — internal capability/prompt-surface disclosure) was public. |
Admin-gated. The only caller (admin Tools tab) already sends the admin JWT. |
| LOW | POST ?wrapup=1 (public lifecycle beacon) had no rate limit — anon status-flip spam. |
Per-IP rate limit. It can't require auth (it fires on pagehide), but beacons are rare per visitor so a window is invisible to real use. |
| LOW | Dead safe variable in commission isAdmin. |
Removed. |
GET ?selftest=1exposes aggregate row counts (not PII) to anonymous callers. Kept public becauseSETUP.md's verification checklist relies on it to confirm "the schema is applied" before sign-in works. Per-user and admin detail are correctly gated.POST ?reengage=1andPOST ?waitlist=1are public but rate-limited — an outreach line (paid model, 90-token cap) and a waitlist insert. Spam surface only; by design.- CORS falls back to
*only whenALLOWED_ORIGINSis unset; the deploy sets it to the site origin in production. Auth is bearer-token, not cookies, andAllow-Credentialsis not set, so a wildcard is not a credential-theft vector. - ReDoS on admin-authored eval regexes (
concierge_evals) — compiled and run in the admin's own browser / the CLI, gated by that table's admin-write RLS. - Server-side
cityvalidation is length-only. The client XSS sink is now encoded (the correct primary fix); tightening the server validation to reject control/markup characters is a defense-in-depth follow-up.
- Security Architecture & Engineering — the database's Row-Level Security is
the authorization boundary. All 24 tables have RLS enabled; owner-scoped tables
(
orders,customers) expose only the caller's rows; admin tables are gated by theis_concierge_admin()security definerhelper; the super-admin row cannot be removed or demoted. Tables with RLS-on-but-no-policy (allocation_counter,serial_holds,rate_limits) are deliberate silent denies, reached only viasecurity definerRPCs / the service role. - Identity & Access Management — every mutating or private-data endpoint is
gated USER / ADMIN / SERVICE.
?judgeand?secretsare admin-only;?custresendis service-to-server only (unreachable from the browser); order writes are ownership-scoped and audited. Auth is passwordless magic-link (no password store). - Secure Software Development (OWASP) — A01 access control enforced in the
DB, not just the UI; A02 secrets server-only, TLS by the platform; A03
no exploitable injection (numeric coercion, whitelists, UUID validation,
encodeURIComponent+ character stripping) and XSS closed bytextContent/ output encoding, with concierge link tokens gated by anhttps:/mailto:allowlist. - Cryptography & Secrets —
ANTHROPIC_API_KEY, the service-role key,RESEND_API_KEY, andSUPABASE_DB_URLexist only inDeno.envinside the edge functions; none appears in any browser asset or response body or log. The key committed in the frontend is the publishable key (browser-safe by design).GET ?secrets=1reports presence booleans only, never values. - Security Assessment & Testing — the behavior-eval deck (
evals/) doubles as a guardrail-regression harness; this audit is repeatable (three scoped passes). - Security Operations / Monitoring — append-only
order_events(field-level diffs) +concierge_actions(every tool call) +email_log; each reply logs the exact model that produced it; all public write/compute paths are rate-limited.
No secret value is ever entered in the browser (the admin page is public).
Values are set in the Supabase dashboard (Edge Functions → Secrets) or as
GitHub secrets that the Deploy Concierge workflow applies with
supabase secrets set. The admin panel can only ever report presence via the
admin-gated ?secrets=1. See SETUP.md for the per-key setup.
| Key | Where | Required |
|---|---|---|
SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY |
auto-injected by Supabase | — |
ANTHROPIC_API_KEY |
Supabase secret / GitHub secret | Yes (chat) |
| SMTP (Auth → dashboard) | Supabase dashboard | Yes (sign-in email) |
RESEND_API_KEY, EMAIL_FROM |
Supabase secret / GitHub secret | Optional (order email) |
ALLOWED_ORIGINS |
set by the deploy workflow | Recommended (CORS) |
| publishable key | committed in the frontend (public by design) | — |
Mostly process, not code: written security policies, periodic access reviews, centralized logging with alerting/monitoring, vulnerability management + periodic penetration testing, vendor/risk management, and change management. For a production deployment the pragmatic path is GDPR/CCPA hygiene + SOC 2 Type II (when an enterprise customer first asks), while architecting to avoid PCI by never handling raw card data (use a hosted processor).