# Oshun v1 — Audit pass 4 gap findings

Date: 2026-05-28 Owner: platform team Previous passes:
[v2](./2026-05-28-v2-gap-audit.md), [v3](./2026-05-28-v3-gap-audit.md)

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+.
