Disciplines · Audits

Oshun v1 — Audit pass 4 gap findings

ending in .allowed.com gets the OAuth code routed to their hostname.

5sections7 minread

On this page

Date: 2026-05-28 Owner: platform team Previous passes: v2, v3

Audit pass 3 closed tasks #119–#145. This fourth sweep used seven parallel read-only explore agents over surfaces the prior three passes didn't deeply cover, plus a regression check against the 40 commits landed during pass 3. 103 net-new actionable findings.

Scope#

Agent Findings Top themes
pass-3 regressions 5 OAuth endsWith suffix-match bypass; gpu-dispatcher IPv6 dual-stack; jwt trap
dependencies 14 axios <1.7.4 CVE-2024-6032; 21 workflows leak secrets on PR; 5 unpinned actions
a11y / i18n 25 3 div-onClick without keyboard; missing aria-label; outline:none w/o focus-visible
observability 20 6 third-party APIs unmetered; queue workers missing metrics; trace propagation
api contract 20 PUT-accepts-Partial across 5 configs; as casts replace zod; .passthrough() abuse
compliance / audit 7 aphrodite moderation skips audit_log; content delete misses search index
resource / DoS 12 maat WebSocket JSON parse no depth; iris GraphQL no depth/complexity guard

Critical regressions in audit-pass-3 work (must fix in-session)#

Reg-1 — OAuth redirect_uri suffix-match collision#

apps/lilith/svc-auth/src/auth-missing-endpoints.ts:194

ts
return allowlist.some((s) => host === s || host.endsWith(`.${s}`));

host.endsWith(.${s}) accepts evil.example.com.allowed.com when allowed.com is in the allowlist. An attacker who controls a parent domain ending in .allowed.com gets the OAuth code routed to their hostname.

Fix: parse to URL, compare url.hostname === s || url.hostname.endsWith(.${s}) with strict equality on the labels, not the string — or use canonical-host comparison.

Reg-2 — GPU dispatcher IPv6 dual-stack bypass#

apps/infra/gpu-dispatcher/src/callbacks.ts:182-188

The IPv6 branch checks loopback ::1, ULA fc/fd, and link-local fe80 — but it does NOT recognise IPv4-mapped IPv6 like ::ffff:192.168.0.1. A callback URL of http://[::ffff:192.168.0.1] bypasses the SSRF guard and reaches internal services.

Fix: detect ::ffff: prefix, extract the embedded IPv4, recurse into the v4 private-range check.

Reg-3 — JWT deprecation trap (audit pass 3 #139)#

apps/lilith/svc-auth/src/jwt-utils.ts:42-51 reads payload.ipAddress / payload.userAgent for anomaly detection. After audit pass 3 #139 marked these fields deprecated on TokenPayload, new tokens omit them — which makes unusualLocation and deviceMismatch always false for new tokens, silently disabling the forensic signal.

Fix: rewrite the anomaly check to look up the session record by sessionId and pull the stored IP / UA from there.

High-severity findings filed as new tasks#

Security / dependency#

# File:line Finding
Sec-1 apps/lilith/svc-multi-region-resilience/package.json:21 axios ^1.6.2 (<1.7.4) — CVE-2024-6032 SSRF in error messages
Sec-2 libs/iris/knowledge/enterprise/package.json:25 axios ^1.6.0 peerDep, same CVE surface
Sec-3 libs/lilith/service-lib/package.json:13 axios ^1.7.7, catalog drift
Sec-4 .github/workflows/ci.yml:3-22,37 + 20 sibling workflows NX_CLOUD_ACCESS_TOKEN set in top-level env: on pull_request — fork PR can exfiltrate
Sec-5 .github/workflows/container-build.yml aquasecurity/trivy-action@master — mutable tag
Sec-6 .github/workflows/iris-container-build.yml aquasecurity/trivy-action@master
Sec-7 .github/workflows/psyche-container-build.yml aquasecurity/trivy-action@master
Sec-8 .github/workflows/security-deps-and-secrets.yml trufflesecurity/trufflehog@main
Sec-9 .github/workflows/v3-workspace.yml snyk/actions/node@master

Resource exhaustion / DoS#

# File:line Finding
Ex-1 apps/maat/api-gateway/src/realtime/websocket-server.ts:144 JSON.parse(data) no depth limit on WS client msgs
Ex-2 apps/iris/api/src/graphql/index.ts:6 createYoga lacks depth-limit / complexity validation
Ex-3 apps/iris/dashboard/src/services/dashboard-data-service.ts:215 toCsv() no MAX_ROWS cap, no streaming
Ex-4 apps/maat/api-gateway/src/realtime/websocket-server.ts:219 broadcastToRoom() no backpressure on bufferedAmount
Ex-5 apps/yemaya/api/src/routes/storyboards.ts:194 Promise.all(panel.annotations.map()) unbounded fan-out
Ex-6 apps/lilith/svc-media/src/processing/ffmpeg-processor.ts:149 ffmpeg input buffer no size pre-check

API contract#

# File:line Finding
Ac-1 apps/lilith/svc-cross-chain-bridge/src/app.ts:182,629 PUT-accepts-Partial; as 0x${string} cast without hex validation
Ac-2 apps/lilith/svc-language-detection/src/app.ts:176,250 req.body as { ... } POST body cast, no zod schema
Ac-3 apps/lilith/bff/src/routes/isis-generation-routes.ts:48 JobSubmitBodySchema.passthrough() forwards unknown fields to Isis
Ac-4 apps/arete/api/src/routes/habits.ts:390 evidenceRefs array uses .passthrough() reaching ORM directly
Ac-5 apps/isis/generation-api/openapi.yaml:102 DELETE returns 200 instead of 204
Ac-6 apps/lilith/svc-native-token/src/app.ts:104 + svc-metaverse + 3 others endpoints lack /v1/ prefix
Ac-7 apps/lilith/svc-{ipfs,defi,staking}/src/app.ts:* PUT-accepts-Partial pattern repeats across 4 config services

Compliance / audit#

# File:line Finding
Aud-1 apps/aphrodite/admin/src/moderation/service.ts:328,146,382 executeAction(), bulkApprove(), assignItem() skip audit_log writes
Aud-2 apps/lilith/svc-content/src/content-crud-routes.ts:250 content DELETE soft-deletes but doesn't call searchService.removeContent()
Aud-3 apps/lilith/svc-data-governance/src/app.ts:2300 createAuditEvent() captures ip+ua but drops eventData metadata for DSR events
Aud-4 docs/runbooks/database-backups.md:110 no RTO / RPO SLO target documented
Aud-5 apps/lilith/svc-observability/src/logging-redaction-utils.ts:17 redaction patterns miss api[_-]?key, passport, license

Observability#

# File:line Finding
Obs-1 apps/veritas/audio/src/server.ts:178 X-Request-ID header in CORS but not extracted from inbound
Obs-2 apps/veritas/{audio,video}/src/providers/*.ts 6 third-party API providers (ElevenLabs, HeyGen, AssemblyAI, Deepgram, Whisper, Ghana NLP) — no http_client_duration_ms
Obs-3 apps/veritas/audio/src/queue/worker.ts:172-182 BullMQ worker no job metrics
Obs-4 apps/veritas/video/src/services/broll/cache.ts:87-89 cache has hit/miss counters internally, not exposed to Prometheus
Obs-5 apps/lilith/svc-payment-orchestrator/src/stripe/stripe-client-adapter.ts:50 StripeClient adapter no timing histogram
Obs-6 apps/veritas/{audio,video,social,ingestion}/src/*.ts no slow-request log threshold; no slow-query log threshold

Accessibility / i18n (top 5 of 25)#

# File:line Finding
A-1 apps/oshun/web/src/components/domains/nyx/NyxObservationLog.tsx:694 clickable <div onClick> no keyboard handler, no role="button"
A-2 apps/oshun/web/src/components/domains/nyx/NyxSkyRenderer.tsx:1596 same pattern
A-3 apps/oshun/web/src/app/not-found.tsx:179 search <input> no aria-label
A-4 apps/oshun/web/src/app/atelier/new/AtelierNewSceneForm.tsx:222 + not-found:191 outline: 'none' no :focus-visible replacement
A-5 n/a (project-wide) No i18n library wired — 15+ hardcoded English strings flagged across web app

What's being addressed this pass#

Tracked as #147+ (see TaskList). In-session, immediate-impact subset:

  • Reg-1, Reg-2, Reg-3 (audit-pass-3 regressions — must fix before further pass-3 deployment)
  • Sec-1 + Sec-2 + Sec-3 (axios bump to catalog)
  • Sec-4 (move secrets from top-level env into per-job env on workflows; sweep ~21 yml files)
  • Sec-5..Sec-9 (pin 5 third-party actions to commit SHAs)
  • Ex-2 (iris GraphQL depth + complexity guard via graphql-armor)
  • Ex-3 (iris dashboard CSV row cap)
  • Aud-1 (aphrodite moderation audit_log writes for approve/reject/escalate/assign/bulk)
  • Aud-2 (content DELETE → searchService.removeContent)
  • Aud-5 (extend redaction patterns: api[_-]?key|passport|license)

What's filed for separate work#

Each remaining finding is filed as a discrete task in the tracker (audit pass 4 work expanded from the original closure-only items #147-#157). Tracker IDs landed for the full v4 finding-set:

Finding Task Area
A-1/A-2 #158 div-onClick → button (2 nyx components)
A-3 #159 not-found search aria-label
A-4 #160 outline:none → :focus-visible (2 files)
A-5 #161 i18n library decision + 15+ hardcoded strings
Ac-1 #162 cross-chain-bridge PUT-Partial + hex cast
Ac-2 #163 language-detection req.body cast → zod
Ac-3 #164 isis-generation-routes drop .passthrough()
Ac-4 #165 arete habits evidenceRefs .passthrough() → strict
Ac-5 #166 isis DELETE 200 → 204 alignment
Ac-6 #167 5 lilith services missing /v1/ prefix
Ac-7 #168 4 config services repeat PUT-Partial pattern
Aud-3 #169 DSR createAuditEvent drops eventData metadata
Aud-4 #170 backup runbook RTO/RPO target (needs SLO team)
Obs-1 #171 veritas/audio doesn't read inbound X-Request-ID
Obs-2 #172 6 veritas third-party providers — no http_client_durn
Obs-3 #173 veritas BullMQ workers — no job metrics
Obs-4 #174 broll cache hit/miss not exposed to Prometheus
Obs-5 #175 stripe adapter — no timing histogram
Obs-6 #176 veritas — no slow-request/slow-query log threshold
Ex-1 #177 maat WS JSON.parse no depth limit
Ex-4 #178 maat WS broadcastToRoom no bufferedAmount check
Ex-5 #179 yemaya storyboards unbounded Promise.all fan-out
Ex-6 #180 ffmpeg processor no input size pre-check
(batch) #181 a11y/i18n remaining 20 findings (gated on #161)

Audit pass 5 (#182) was launched as a follow-up adversarial sweep covering test coverage, db/schema integrity, secrets/crypto/env, concurrency races, and error-handling null safety — areas not deeply enumerated by the 7 agents of pass 4. Findings will land as #183+.