CASA Tier 2 (ADA AL1) — Self-Assessment Workbook
App: ERP-Unlocked (Ordermatic) — AI order-processing platform Standard: App Defense Alliance CASA Specification v2.1.1 (2026-06-03), formerly the ADA “Web App Profile” / OWASP ASVS-based. Spec source: https://github.com/appdefensealliance/ASA-WG/tree/main/CASA Assessment date: 2026-06-25 Status: DRAFT — developer self-assessment / gap analysis. Not yet submitted to an ADA-Authorized Lab.
0. What “CASA Tier 2” means and what this document is
Section titled “0. What “CASA Tier 2” means and what this document is”“CASA Tier 2” is the self-scan tier most commonly required by Google for OAuth apps using restricted/sensitive scopes. We request two restricted Gmail scopes for email-pull: gmail.readonly (read inbound purchase-order emails for ingest) and gmail.modify (draft suggested replies for CSRs, apply labels/tags, and move/archive processed order emails). Both are restricted scopes covered by the same Tier 2 assessment; gmail.modify is the minimum scope that supports labelling + moving messages (messages.modify) — gmail.labels only manages label definitions, not message membership. We deliberately do not request https://mail.google.com/ (full access incl. permanent delete). Under the App Defense Alliance’s renamed scheme, Tier 2 maps to Assurance Level 1 (AL1) — “Developer Tested, Lab Reviewed.”
The certification itself is issued by an ADA-Authorized Lab, not self-declared and not issuable by any tool. The path is:
- Define scope — which app(s)/APIs are in scope (§1 below).
- Run the required automated scan — a DAST scan against a live deployment, using ADA’s provided Burp scan config (
CASA/ADA Burp Audit Scan Configuration.json) or an ADA-recognized equivalent (OWASP ZAP, etc.). Remediate Critical/High findings. - Complete the developer questionnaire + test report —
Submission Forms and Templates/Developer Questionnaires/CASA AL0 and AL1 Onboarding Questionnaire.xlsx, providing a statement of compliance and evidence/code snippets for each of the ~38 audit cases. This workbook is the raw material for that. - Engage an ADA-Authorized Lab — they review the evidence against the requirements and, if sufficient, issue the certification. (At AL1 the lab reviews evidence; it does not test the app directly — that’s AL2.)
This workbook does steps 1 and 3’s heavy lifting (scope + per-control gap analysis with code evidence) and gives a prioritized remediation list for step 2.
1. Scope
Section titled “1. Scope”| Item | In scope? | Notes |
|---|---|---|
apps/webapp (customer app, Astro/React, app.ordermatic.co) |
✅ Primary target | Public-facing, handles confidential customer/order data |
apps/webapp API routes (/api/**) |
✅ | First-party web APIs |
apps/internal (admin portal) |
✅ Admin interface | Behind Cloudflare Access |
apps/partner-data-api |
✅ if internet-exposed | Confirm exposure |
Backend workers (temporal-worker, ts-temporal-worker, dagster, email-worker) |
✅ as shared backend | In scope as backend components serving the target app |
| Clerk (auth/IdP) | 3P auth — partial | In scope only for Authn/Session/Access-control requirements (per spec’s 3P scoping). Confirm Clerk’s own ADA-approved-IdP status to delegate §1. |
| Stripe (billing) | 3P — sensitive | In scope for the webhook + payment-data handling it touches |
| Google (Gmail OAuth/Pub-Sub), WhereFour, Prophet21 ERP | 3P APIs | In scope only where they perform sensitive operations the app uses |
| Analytics / non-sensitive 3P | ❌ Out | Per spec scoping guidance |
2. Control matrix
Section titled “2. Control matrix”Legend: ✅ PASS (code evidence control is met) · ⚠️ GAP (confirmed deficiency — remediate) · 🔍 MANUAL (cannot be closed from code review alone — needs live scan / infra / dashboard config / Clerk attestation) · N/A
1 — Authentication
Section titled “1 — Authentication”| # | Requirement | Status | Evidence / Notes |
|---|---|---|---|
| 1.1.1–1.1.3 | Brute-force resistance, secure initial passwords, offline-resistant password storage | 🔍 MANUAL (delegated) | Auth fully delegated to Clerk; no first-party password handling (packages/auth/src/server.ts, middleware.ts). Action: confirm Clerk is an ADA-approved authentication service (lets us delegate §1) or attach Clerk’s CASA attestation. |
| 1.2.1 | No default credentials on public interfaces | ✅ PASS | No seeded/hardcoded accounts. apps/internal dev fallback (x-dev-email) is import.meta.env.DEV-only. |
| 1.3.1–1.3.4 | OOB verifiers random, single-use, expiring, brute-force resistant | 🔍 MANUAL (delegated) | Email verification codes handled by Clerk. Confirm via Clerk attestation. |
2 — Session Management
Section titled “2 — Session Management”| # | Requirement | Status | Evidence / Notes |
|---|---|---|---|
| 2.1.1 | No passwords/session tokens in URL query strings | 🔍 MANUAL | __clerk_db_jwt appears in post-sign-in redirect URLs (packages/auth/src/middleware.ts:304-310). This is a Clerk-managed short-lived handshake token, not the session token. Action: document & confirm with Clerk it’s acceptable; ensure it’s stripped quickly (helper _withoutDbJwt() exists). |
| 2.2.1 | Logout/expiry invalidates all stateful tokens incl. refresh | ✅ PASS (delegated) | clerk.signOut(); app only clears its Redis caches via webhooks. |
| 2.2.2 | Password change terminates other sessions | ✅ PASS (delegated) | Clerk policy. |
| 2.2.3 | Non-revocable stateless tokens expire < 24h | ✅ PASS | Clerk __session JWT lifetime ≈ 60s (well under 24h); no in-house long-lived JWTs minted. |
| 2.3.1 / 2.3.2 | Session cookies Secure + HttpOnly |
✅ PASS | Clerk-managed cookies; custom OAuth state cookie sets HttpOnly; SameSite=Lax; Secure (apps/webapp/src/pages/api/integrations/_oauth-cookies.ts:10-16). |
| 2.3.3 | Prefer session tokens over static API secrets | ✅ PASS | Clerk sessions are primary; static tokens (INTERNAL_SERVICE_TOKEN, EVAL_SERVICE_TOKEN) are service-to-service only. |
| 2.3.4 | Stateless tokens signed/encrypted vs tampering/replay | ✅ PASS | Clerk RS256-signed JWTs verified against JWKS (packages/auth/src/server.ts). |
| 2.4.1 | Full session / re-auth before sensitive actions | ⚠️ GAP (minor) / 🔍 MANUAL | No in-app step-up re-authentication before sensitive actions (ERP writes, account changes). Relies on Clerk MFA-at-login + role checks + feature-flag gating. Action: confirm this is acceptable for AL1, or add a step-up challenge for the most sensitive mutations. |
3 — Access Control
Section titled “3 — Access Control”| # | Requirement | Status | Evidence / Notes |
|---|---|---|---|
| 3.1.1–3.1.3 | Least-privilege on trusted service layer; non-manipulable policy attrs; fail-closed | ✅ PASS | Server-side org-scoped Drizzle queries; default-deny in packages/auth/src/middleware.ts. |
| 3.1.4 | IDOR protection (CRUD scoped) | ✅ PASS (verify via scan) | Strongest area. Every data-access path filters by clerkOrganizationId (e.g. apps/webapp/src/pages/api/orders/[id].ts:95-106; routing-rules, notifications, ERP connections all indexed + scoped). Support-admin bypass is explicit and audited (SupportAccessAudit.logAccess()). Lab will probe this live — keep as the headline evidence. |
| 3.1.5 | Strong anti-CSRF | ✅ PASS | Token-based CSRF (packages/utils/src/csrf/validate-csrf.ts, e.g. organizations/index.ts:68-80) + SameSite cookies. Note: Astro’s global origin-check is disabled for the CF email-worker multipart path — confirm per-endpoint CSRF covers all state-changing routes. |
| 3.1.6 | Directory browsing disabled | ✅ PASS | Astro node adapter, output: 'server'; no filesystem listing. |
| 3.2.1 | Only secure OAuth flows (Auth Code / +PKCE) | ✅ PASS | Clerk handles social login (Auth Code+PKCE). Our own OAuth server (MCP/integration clients) enforces PKCE: .well-known/oauth-authorization-server.ts advertises code_challenge_methods_supported: ['S256'] and api/oauth/authorize.ts:53-54 requires code_challenge (defaulting method to S256). Integration OAuth (HubSpot/Slack/Gmail) uses Auth Code with state in an HttpOnly cookie (integrations/oauth/[provider]/callback.ts). Confirmed 2026-06-29. |
| 3.2.2 | Validate redirect_uri + state |
✅ PASS | State stored/validated via HttpOnly cookie, 10-min TTL; redirect_uri server-constructed. |
| 3.3.1 | Admin interfaces use MFA | 🔍 MANUAL | apps/internal requires Cloudflare Access header; MFA is enforced at the CF edge. Action: confirm the CF Access policy mandates MFA. |
4 — Communications
Section titled “4 — Communications”| # | Requirement | Status | Evidence / Notes |
|---|---|---|---|
| 4.1.1 | TLS 1.2+ all connections, secure ciphers | 🔍 MANUAL | Cloud Run terminates TLS at edge. No plain-http:// outbound found in app code. Action: capture a Qualys SSL Labs “A” report for each public host as evidence. |
| 4.1.2 | Trusted TLS certs (no validation bypass) | ⚠️ GAP | rejectUnauthorized: false on Redis TLS (packages/utils/src/redis.ts:166) and a Postgres script (packages/db/scripts/backfill_prod_orgs.ts:7). Action: supply the provider CA bundle / pin instead of disabling validation. |
| 4.1.3 | No weak cryptography | ✅ PASS | No MD5/SHA1/DES/RC4/ECB for security. Math.random() uses are non-security (quote/correlation IDs). |
| 4.1.4 | Crypto fails securely (no padding oracle) | ✅ PASS | AES-256-GCM (authenticated) + PBKDF2-HMAC-SHA512 for ERP cred decryption (apps/dagster/erp_pipeline/sources/erp_auth.py:91-105). |
5 — Data Validation & Sanitization
Section titled “5 — Data Validation & Sanitization”| # | Requirement | Status | Evidence / Notes |
|---|---|---|---|
| 5.1.1 | HTTP parameter pollution | ✅ PASS (verify via scan) | Zod schema validation on endpoints. |
| 5.1.2 | Redirects allowlisted | ✅ PASS | Redirects use static internal paths; no user-controlled next/redirect_uri to Location. |
| 5.1.3 | No unsafe eval()/dynamic exec |
✅ PASS | No eval/new Function/exec/pickle.loads/unsafe yaml.load. |
| 5.1.4 | Template injection | ✅ PASS | AI/email prompts are static templates; structured data flows into Pydantic models, not into template strings. |
| 5.1.5 | SSRF prevention | ✅ PASS (caveat) | Outbound calls (P21, WhereFour) use stored org credentials, not user-supplied URLs; WhereFour fetch blocks localhost/loopback (apps/dagster/erp_pipeline/sources/wherefour_fetch.py:47-67). Action: confirm no user-controlled URL reaches a server-side fetch. |
| 5.1.6 | XPath/XML injection | ✅ PASS | No untrusted XML parsing; P21 XML is string-built, not parsed from user input. |
| 5.1.7 | Context-aware output escaping (XSS) | ✅ PASS (verify via scan) | React auto-escaping; no dangerouslySetInnerHTML / Astro set:html; email bodies not rendered as HTML in UI. |
| 5.1.8 | DB injection | ✅ PASS | Drizzle ORM parameterized; Python uses %s params. sql.raw() only in dev/investigation scripts with hardcoded table-name enums (not user-reachable). |
| 5.1.9 | OS command injection | ✅ PASS | No shell=True/os.system/child_process.exec with user input. |
| 5.1.10 | LFI/RFI / path traversal | ✅ PASS | R2 keys built with random nanoid() prefix; document fetch uses DB-stored key, not user path. |
| 5.2.1 | Safe untrusted file uploads | ✅ PASS (harden) | MIME allowlist (PDF/CSV/Excel/text/image), server-side type determination, random R2 key, non-executable object storage, async processing (apps/webapp/src/pages/api/pdf-documents/index.ts). Hardening: add magic-byte signature validation. |
6 — Configuration
Section titled “6 — Configuration”| # | Requirement | Status | Evidence / Notes |
|---|---|---|---|
| 6.1.1 | No components with known exploitable vulns | ✅ PASS | Automated CVE/SCA scanning in CI for both ecosystems: Node via pnpm audit --audit-level=critical (validate.yml:65) and Python via pip-audit over the uv-locked envs (.github/workflows/python-sca.yml). The Python gate is blocking and clean — the original ~41-CVE backlog was remediated by bumping the locks (see §3b); pip-audit reports no known vulnerabilities for either the root workspace or apps/dagster. The pnpm supply-chain gate (scripts/verify-pnpm-controls.sh: 7-day cooldown, exotic-subdep block, build-script allowlist) remains as additional hardening. |
| 6.2.1 | Debug modes off in prod | ✅ PASS | No debug=True/reload=True in prod; log level defaults to info; no source-map leak. |
| 6.3.1 | Origin header not used for authz | ✅ PASS | Origin used only for CORS preflight against a fixed allowlist; authz is Clerk session. |
| 6.4.1 | Subdomain takeover protection | 🔍 MANUAL | DNS/infra control. Action: run a dangling-CNAME/takeover scan across ordermatic.co subdomains. |
| 6.5.1 | No logging of credentials/payment data | ✅ PASS (harden) | Active PAN/CVV/expiry scrubbing with Luhn check (wherefour_fetch.py); Redis URL masking (redis.ts:332-354); no plaintext secret logging found. Hardening: add a central Pino redaction serializer rather than per-call discipline. |
| 6.6.1 | Browser storage cleared on logout | ✅ PASS | Only non-sensitive UI/cache state in local/session storage; Clerk clears its own session. |
| 6.7.1 | Server-side secrets stored securely | ✅ PASS | Secrets via Infisical + 1Password op. .env is gitignored (.gitignore:23) and was never committed (verified git log --all). No secrets in version control. |
7 — Webhook Security
Section titled “7 — Webhook Security”Inbound inventory: Stripe (/api/webhooks/stripe), Clerk/Svix (/api/webhooks/clerk), Cloudflare inbound-email (/api/webhook/inbound-email), forwarding-confirmation (/api/webhook/forwarding-confirmation), Cloudflare attachment upload (POST /api/pdf-documents with source=external). Outbound to tenant URLs: none.
Gmail Pub/Sub push retired (2026-06-25):
/api/webhook/gmail, itsadapter.ts, and the/api/watch/gmail/*registration endpoints were deleted — Gmail ingest now runs via Temporal polling (activities/gmail.py), which makes no inbound web request, so it falls entirely outside §7. This removed the only unauthenticated webhook in the app rather than hardening it.
| # | Requirement | Status | Evidence / Notes |
|---|---|---|---|
| 7.1.1 | Webhooks over HTTPS/TLS 1.2+ | ✅ PASS | All endpoints HTTPS. |
| 7.1.2 | Provider verifies endpoint ownership | N/A | App receives only; no outbound delivery to tenant URLs. |
| 7.2.1 | Inbound payloads HMAC-verified | ✅ PASS | Stripe (constructEvent), Clerk (svix Webhook.verify), inbound-email + forwarding-confirmation + external attachment upload (verifySignature HMAC-SHA256) all verify before processing. The attachment upload signs {filename, connectionId, organizationId} rather than a raw body, and additionally requires the signed organizationId to own the connection. The previously-flagged unauthenticated Gmail Pub/Sub endpoint was retired, not present. |
| 7.2.2 | Timing-safe comparison | ✅ PASS | SDKs use timing-safe compare; inbound-email uses crypto.subtle.verify. |
| 7.2.3 | Replay protection via signed timestamps | ⚠️ GAP (low) | Clerk/Svix (5-min window) + Stripe ✅. Inbound-email is idempotency-only (no signed timestamp): its messageId guard blocks duplicate processing. The external attachment upload (POST /api/pdf-documents) has neither a signed timestamp nor an idempotency key. It mints a fresh R2 key and row per request, so a captured signed body replays into extra pending documents. Action: §3a recipe for the webhook (deferred); the signed-manifest follow-up for the upload is tracked in TODOS.md under ## Security. |
| 7.3.1 | SSRF mitigation for user-supplied callback URLs | N/A | No user-supplied callback URLs. |
| 7.3.2 | Signing secrets not hardcoded | ✅ PASS | All loaded from env / secrets manager (apps/webapp/src/lib/config.ts). |
3. Prioritized remediation punch list (developer-side, before submission)
Section titled “3. Prioritized remediation punch list (developer-side, before submission)”| P | Control | Item | Status |
|---|---|---|---|
| P1 | 7.2.1 / 7.2.3 | Unauthenticated Gmail Pub/Sub push endpoint | 🗑️ RETIRED — /api/webhook/gmail + adapter.ts + /api/watch/gmail/* deleted (Gmail ingest is Temporal polling now). No JWT/Pub/Sub config needed; the endpoint no longer exists. Supersedes the earlier “harden with OIDC JWT” fix. |
| P1 | 6.1.1 | Automated SCA/CVE scanning | ✅ DONE — Node covered by pnpm audit --audit-level=critical (validate.yml:65); Python by .github/workflows/python-sca.yml (uv sync + pip-audit). The ~41-CVE backlog was cleared (locks bumped — see §3b) and the gate is now blocking (continue-on-error removed), so new vulnerable deps fail CI. pip-audit is clean for both the root workspace and apps/dagster. 6.1.1 met. |
| P2 | 4.1.2 | Remove rejectUnauthorized: false |
✅ DONE — packages/utils/src/redis.ts (validate by default; REDIS_CA_CERT for private CAs; REDIS_TLS_INSECURE break-glass) + packages/db/scripts/backfill_prod_orgs.ts. Deploy step: if prod Redis uses a private CA (GCP Memorystore), set REDIS_CA_CERT before deploy. |
| P2 | 7.2.3 | Signed-timestamp replay protection on inbound-email webhook | ⏸️ DEFERRED (see §3a) — already mitigated by the messageId idempotency guard; the real fix is a coordinated worker↔webapp HMAC change with deploy-skew risk. |
| P3 | 2.4.1 | Step-up re-auth for sensitive mutations (or document Clerk-MFA reliance) | Open — decision needed |
| P3 | 2.1.1 | Document __clerk_db_jwt URL handoff as accepted Clerk mechanism |
Open — doc only |
| P4 (harden) | 5.2.1 | Magic-byte file-signature validation on uploads | Open |
| P4 (harden) | 6.5.1 | Central Pino redaction serializer | Open |
| P4 (harden) | 7.2.2 | Timing-safe Gmail GET token compare (adapter.ts:42) |
🗑️ RETIRED — adapter.ts was deleted with the Gmail Pub/Sub push path (see 7.2.1 note); the GET token compare no longer exists. |
3a. Deferred: inbound-email replay protection (7.2.3) — ready-to-apply recipe
Section titled “3a. Deferred: inbound-email replay protection (7.2.3) — ready-to-apply recipe”The worker (apps/email-worker/src/webapp-client.ts sign/signedHeaders) and the receiver (apps/webapp/src/lib/hmac.ts verifySignature, used by /api/webhook/inbound-email and /api/alias/lookup) sign the body only. Replay protection requires a signed timestamp, which touches both sides — and they deploy independently (Cloudflare worker vs. Cloud Run), so a naive change breaks auth during deploy skew.
Apply when ready, deploy webapp first:
- Worker: send
X-Inbound-Email-Timestamp: <ms>and sign${ts}.${body}. - Receiver: if the timestamp header is present, verify over
${ts}.${body}and reject if|now - ts| > 5min; else fall back to body-only (back-compat for the skew window). - Once the worker is deployed everywhere, drop the body-only fallback.
Not urgent: the existing messageId+provider idempotency guard (inbound-email.ts:76-92) already blocks duplicate processing, so a replayed payload can’t create duplicate rows.
3b. Python CVEs surfaced by the new gate (6.1.1 work) — REMEDIATED
Section titled “3b. Python CVEs surfaced by the new gate (6.1.1 work) — REMEDIATED”The gate originally flagged ~41 CVEs (42 root + 16 dagster) across the locked deps — mostly transitive in dagster/temporal-worker. All are now fixed: pip-audit reports no known vulnerabilities for either workspace. Direct deps had their floors raised in the relevant pyproject.toml; transitives were bumped with uv lock --upgrade-package; pillow needed a [tool.uv] override-dependencies entry because mirascope[google] caps it <11 while the fixes ship only in 12.x (and customer-emailed images flow through pillow OCR, so the CVEs are reachable). No CVE required an --ignore-vuln exception — every advisory had a compatible fix. Tests pass: dagster 540, temporal-worker 408, pdf-shared 469.
Root workspace (uv.lock — temporal-worker + pdf-shared):
| Package | Was | Now | Advisory | How |
|---|---|---|---|---|
| aiohttp | 3.13.3 | 3.14.1 | CVE-2026-22815, -34513…-34525, -34993, -47265, -50269, -54273…-54280 | upgrade-package |
| cryptography | 46.0.5 | 49.0.0 | PYSEC-2026-35, -36, GHSA-537c-gmf6-5ccf | upgrade-package |
| idna | 3.11 | 3.18 | PYSEC-2026-215 | upgrade-package |
| pillow | 10.4.0 | 12.2.0 | CVE-2026-25990, -40192, -42310, -42311, PYSEC-2026-165 | override (≥12.2.0) |
| pyasn1 | 0.6.2 | 0.6.3 | CVE-2026-30922 | upgrade-package |
| pydantic-settings | 2.13.1 | 2.14.2 | GHSA-4xgf-cpjx-pc3j | floor (direct) |
| pygments | 2.19.2 | 2.20.0 | CVE-2026-4539 | upgrade-package |
| pytest | 9.0.2 | 9.1.1 | CVE-2025-71176 | floor (direct dev) |
| python-dotenv | 1.0.1 | 1.2.2 | CVE-2026-28684 | floor (direct dev) |
| requests | 2.32.5 | 2.34.2 | CVE-2026-25645 | upgrade-package |
| urllib3 | 2.6.3 | 2.7.0 | PYSEC-2026-141, -142 | upgrade-package |
Dagster (apps/dagster/uv.lock):
| Package | Was | Now | Advisory | How |
|---|---|---|---|---|
| aiohttp | 3.13.5 | 3.14.1 | CVE-2026-34993…-54280 | upgrade-package |
| cryptography | 48.0.0 | 49.0.0 | GHSA-537c-gmf6-5ccf | upgrade-package |
| msgpack | 1.1.2 | 1.2.1 | GHSA-6v7p-g79w-8964 | upgrade-package |
| pydantic-settings | 2.14.1 | 2.14.2 | GHSA-4xgf-cpjx-pc3j | floor (direct) |
| starlette | 1.1.0 | 1.3.1 | CVE-2026-54282, -54283 | upgrade-package |
4. MANUAL / external evidence to gather (cannot be closed from code)
Section titled “4. MANUAL / external evidence to gather (cannot be closed from code)”- DAST scan against a live deployment using ADA’s Burp config (or ZAP) — primary evidence for §5 and IDOR (3.1.4). Triage to zero Critical/High.
- ✅ DONE — authenticated active scan (OWASP ZAP 2.17.0, an ADA-recognized equivalent of the Burp config) against
staging.ordermatic.co, 2026-06-30. Result: 0 High/Critical; no injection, authz-bypass, or path-traversal findings. 14 distinct alert types across 369 requests / 3,587 instances.- Method (this is a genuinely authenticated scan): signed in as a real test account (Clerk session); a 30-second refresher minted fresh
Authorization: BearerJWTs from the live session into a ZAP replacer rule, so every scan request carried a valid token — verified200(not401) on/api/**throughout. Seeded the full first-party API surface (263/api/**routes from the repo) plus real parameterized calls harvested by driving the authenticated app (inbox, customers, analytics…); full active scan policy, 4 threads/host, 40-min cap. The scan ran as a non-admin account, so admin/destructive endpoints correctly returned403— i.e. authorization is enforced server-side, not just hidden in the UI. - Medium (488 inst): every Medium is CSP header-hardening (CWE-693: no/weak Content-Security-Policy — wildcard,
unsafe-inline,unsafe-eval). Not an exploitable-vuln class; tracked as the P4 header item below. - Low (200 inst):
Strict-Transport-Securitynot set on staging (prodapp.ordermatic.cosends HSTS);oauth_integration_statecookie missingSecureon staging — it isHttpOnly; SameSite=Laxon every env andSecureonly whenNODE_ENV=production(apps/webapp/src/pages/api/integrations/_oauth-cookies.ts), so prod is fine; recommend gatingSecureon the request being HTTPS rather than onNODE_ENVso HTTPS non-prod envs also get it. “Application Error Disclosure” 500s returned a generic message (no stack trace / internals leak — confirmed) and several are seed artifacts (colocated*.test.tspaths that aren’t real routes). Timestamp-disclosure entries are scanner noise. - Informational (2,899 inst): dominated by ZAP’s User-Agent-Fuzzer and Suspicious-Comments (TODO/FIXME in JS bundles) — scanner noise, no action.
- Artifacts: full ZAP report retained out-of-repo (
zap-authenticated-active-staging.html/.md).
- Method (this is a genuinely authenticated scan): signed in as a real test account (Clerk session); a 30-second refresher minted fresh
- Prior step — unauthenticated baseline (2026-06-30): 0 FAIL, 57 PASS, 10 WARN (header hardening only) on the public/sign-in surface. Superseded by the authenticated active scan above.
- ✅ DONE — authenticated active scan (OWASP ZAP 2.17.0, an ADA-recognized equivalent of the Burp config) against
- Clerk attestation / confirmation it’s an ADA-approved authentication service → delegates §1 and parts of §2.
- Qualys SSL Labs “A” report per public host (4.1.1).
- Cloudflare Access policy screenshot showing MFA required for
apps/internal(3.3.1). - Subdomain-takeover scan of ordermatic.co (6.4.1).
- ✅ Quick scan clean (2026-06-30) — dig CNAME/A across known
ordermatic.co/erpunlocked.comhosts found no dangling CNAMEs. Live third-party CNAMEs (cal → members.tidycal.com,b → *.proxyhog.com) point to active services — keep those accounts current. Other referenced names (grafana, inbox, security, etc.) have no DNS record at all (nothing to take over). Note: known-host check only — run a full subfinder/subjack enumeration before submission.
- ✅ Quick scan clean (2026-06-30) — dig CNAME/A across known
- Confirmation PKCE is enabled on integration OAuth flows (3.2.1).
- ✅ CONFIRMED (2026-06-30) — see §2 control 3.2.1: OAuth server advertises
code_challenge_methods_supported: ['S256']andapi/oauth/authorize.tsenforcescode_challenge.
- ✅ CONFIRMED (2026-06-30) — see §2 control 3.2.1: OAuth server advertises
Security-header hardening (surfaced by both ZAP scans — low priority, below the AL1 Critical/High bar)
Section titled “Security-header hardening (surfaced by both ZAP scans — low priority, below the AL1 Critical/High bar)”Add response headers at the edge / app: Content-Security-Policy (the only Medium-risk class in the authenticated active scan — currently wildcard / unsafe-inline / unsafe-eval), Permissions-Policy, Cross-Origin-Embedder-Policy (+ COOP/CORP), and Strict-Transport-Security on staging (prod already sends it). These are WARN/Medium-but-non-exploitable (not FAIL) so they don’t block AL1, but they’re cheap defense-in-depth and account for every non-informational finding once the scanner noise is set aside. Track as a P4 hardening item.
5. Bottom line
Section titled “5. Bottom line”The application is in good shape for CASA Tier 2 / AL1. Access control and multi-tenant isolation (the highest-risk family for a multi-tenant ERP) are strong, injection defenses are solid, and secrets are not in version control. The required DAST scan is done — an authenticated active ZAP scan of the full /api/** surface returned 0 High/Critical and no injection/authz/traversal findings (§4); the only non-noise results are CSP/HSTS header hardening (P4). The remaining work is the external evidence in §4 that can’t come from code or a scan (Clerk’s ADA-auth attestation, SSL Labs “A” per host, the Cloudflare Access MFA screenshot) plus the small P3/P4 hardening items — then engaging an ADA-Authorized Lab to review the questionnaire and issue the certification.