Security Findings Register — Phase 7
Status: Draft (P7 WP5 deliverable)
This is the triage register for the Phase 7 adversarial security review (plan §4 / §8 WP5). It records every finding from the red-team suites and the fresh-eyes review of the five named surfaces (session admin operations, cross-DO transport, sandbox-local, MCP connector, demo/live profiles), each with an owner and a status in fixed / accepted-risk(reason) / deferred(gate).
The threat model itself is docs/THREAT-MODEL.md; the normative controls live in docs/spec/security-operations.md.
Mechanical exit check
The P7 exit gate "security review has no unowned critical finding" is discharged mechanically:
- Zero rows lack an owner. Every row below names an owner.
- Zero critical rows are unfixed. No row is severity
criticalorhigh; the highest ismedium. There are therefore no unfixed critical findings.
Scope honesty: this review covers the framework packages as shipped plus the Travel Planner / docs-researcher / repo-ops reference assemblies. It does NOT cover the hosted Cloudflare service (not claimed at P7), real supplier APIs (deterministic desk only), or application-owned authentication. The review is a fresh-eyes framework audit, not a penetration test of a deployed system.
Red-team suites (executable evidence)
These suites PIN the controls the review relied on. They are green in bun run test.
| Suite | Surface | What it proves |
|---|---|---|
packages/testing/test/security/redteam-supplier-injection.test.ts | supplier content, tool safety | Prompt-injected supplier content cannot escalate capability: the demanded mutation is approval-gated and never reaches the supplier desk; an operator denial settles the Run failed with zero supplier calls; the injected credential is untrusted tool output that grants nothing and is stripped by the Redactor. |
packages/testing/test/security/redteam-child-exfiltration.test.ts | subagent join, redaction | A durable child's fetched-body secret and raw notes never cross the delegation boundary — only the bounded declared summary does; the SubagentExecutionFailure projection has no Cause/stack channel and rejects over-length payloads; the Redactor strips secret scalars from a child failure/progress preview. |
packages/testing/test/security/redteam-idor-sweep.test.ts | admin surface, tenancy | Under a tenant-scoped OperationAuthorizer, every targeted admin operation (observe/explain/explainConversation/verify/retry/wake/resolveUnknown/resolveApproval) denies a foreign Conversation or Submission fail-closed and permits the caller's own; every operation reaches the authorization seam. |
packages/session/src/ledger-conformance.ts → crossPrincipalAdmissionScoping | tenancy, idempotency | A second principal reusing another principal's (conversation, idempotency key) mints a fresh distinct Submission — never a replay, collision, or cross-principal discovery (SEC-002 "collide" half). Runs against every ledger adapter (memory / SQLite / DO). |
Findings
| ID | Surface | Severity | Summary | Owner | Status |
|---|---|---|---|---|---|
| SEC-P7-001 | MCP connector | medium | MCP discovery canonicalization/digest (capabilities/src/mcp.ts canonicalJson) recurses with no depth bound, so an adversarial or compromised MCP server can serve a deeply-nested tool inputSchema and stack-overflow the connecting worker fiber before the 1 MB discovery-bytes bound is enforced. SEC-013 / testing.md §10 call for a malicious-Schema depth/size defense; redaction.ts already demonstrates the team's own MAX_REDACTION_DEPTH guard pattern the MCP path lacks. Confirmed by probe: a naive recursive canonicalizer throws RangeError: Maximum call stack size exceeded on deep input. | P7-integrator | deferred(gate: add a recursion-depth bound to canonicalJson/digestJson mirroring MAX_REDACTION_DEPTH, and enforce maxDiscoveryBytes before canonicalization — before any open-source MCP exposure) |
| SEC-P7-002 | Cross-DO transport | medium | The Conversation Object portCall endpoint (platform-cloudflare/src/conversation-object.ts) executes every decoded closed-subset PortRequest — including StoreExport (full canonical log read), StoreReadPage, LedgerLookup, LedgerAdmit, StoreAppend, LedgerRequestAbort — against the local facets with NO OperationAuthorizer check and no principal. Any code holding the DurableObjectNamespace binding can read or mutate any Conversation's lane. This is the designed sibling-Object channel; the namespace binding IS the trust boundary. It is a wider internal surface than the runtime's authorizer-gated observe/admin operations, and it is recorded rather than silently assumed. | P7-integrator | accepted-risk(the cross-DO port is a system-to-system channel between sibling Objects of one deployment; the Cloudflare namespace binding is a Worker-level capability never handed to end users; adding per-request authorization would break the S2 cross-Object delegation protocol. Tenant/principal isolation on this channel is a hosted-platform concern, not claimed at P7.) |
| SEC-P7-003 | Session admin ops / DC | medium | Admin operations default to possession authorization (the pre-P7 behavior). On DC, the four Conversation Object admin entry points (explainEncoded/verifyEncoded/retryEncoded/obligationsEncoded) always run the default possession authorizer — there is no CloudflareDurableRuntimeOptions lever to install a non-default OperationAuthorizer — so admin-surface tenant isolation on DC is gated only by who can invoke the DO method (the namespace binding, see SEC-P7-002). The code states this honestly (conversation-object.ts lines ~322-327). | P7-integrator | accepted-risk(possession default is the documented D10 stance; DN hosts install a non-default authorizer through operationAuthorizerLayer; a DC authorizer config lever is additive and deferred with the hosted-platform work. The IDOR red-team suite proves the enforcement path works when an authorizer is present.) |
| SEC-P7-004 | Session admin ops | low | The submission-scoped authorization requests for retry, resolveUnknown, resolveApproval, and explain (session/src/durable-runtime.ts) carry only submissionId, not conversationId, so a tenant-scoped host authorizer must resolve the Submission→tenant mapping itself to make an IDOR decision on those operations. observe/verify/wake/explainConversation do carry conversationId. | P7-integrator | deferred(gate: additively include conversationId in the OperationAuthorizationRequest for the submission-scoped operations after a cheap ledger lookup, so a tenant authorizer needs no side lookup — before the authorizer port is declared stable. The red-team idor sweep denies these paths today by modelling the host-side submission→tenant resolution.) |
| SEC-P7-005 | Subagent join | low | The durable child-failure projection (durable-runtime.ts boundedChildFailureResult) forwards a failed child's own declared settlement result verbatim (bounded to the canonical persistence limit) rather than through the delegation's mapChildFailure declassification, which only runs on the ephemeral path. A child's declared failure output therefore crosses to the parent join as-is. No raw Cause, stack, or child transcript crosses (proven by redteam-child-exfiltration); the child holds only its narrowed least-authority context, so it can leak only what it was given. | P7-integrator | accepted-risk(documented in capabilities/src/subagent.ts as a recorded later extension: "the typed child failure union does not survive a durable Settlement, so mapChildFailure remains the ephemeral-path contract; Schema-declared durable domain-failure mapping is a recorded later extension". Residual exposure is bounded and within the parent's own trust domain.) |
| SEC-P7-006 | Sandbox-local | low | @effect-agent/sandbox-local is an unisolated local process runner, correctly and loudly labeled isolation: "unisolated" and rejecting every isolation-requiring request (mounts, network, cpu/memory limits, secret handles, artifacts) fail-closed. The repo-ops AuditReportSink.layerFileSystem write path composes ${reportRoot}/${reportPath} after normalizeRepoRelativePath (which blocks absolute paths, .., ., empty segments, backslash, spaces), so model-supplied path traversal is closed; the residual is that FileSystem follows a pre-existing symlink under reportRoot. | P7-integrator | accepted-risk(the local sandbox is development-only tooling by contract, CAP-010/security-operations §9; the report sink writes into a caller-owned reportRoot (a temp dir in tests); model path traversal is closed. A production sink would resolve symlinks under the jail root — noted for the eventual isolated adapter, not the dev runner.) |
| SEC-P7-007 | Artifact classification | N/A | Cross-tenant artifact classification laundering (testing.md §10) has no reachable surface: no AttachmentStore / artifact port exists in the framework as shipped. Oversized transported values fail typed at the value bound rather than becoming misclassified artifacts. Recorded explicitly as N/A rather than silently omitted. | P7-integrator | accepted-risk(no artifact port exists to attack; revisit when an AttachmentStore is introduced during open-source preparation.) |
Reviewed and found sound (no defect filed)
These were probed during the review and are recorded as analyzed-safe so a later reviewer need not re-derive them:
- Routable Submission identity parsing (
storage-cloudflare/src/routing.tsroutableSubmissionTarget): a crafted{uuidv7}:{conversationId}can route a closed-subset call to a named foreign Object, but that is exactly the capability already reachable throughportCall(SEC-P7-002); it grants nothing new. Over-length identities fail typed; malformed identities fall back to the local facet, which is the only authority the Object can consult without inventing an owner. - Prompt-injection to unapproved mutation (
redteam-supplier-injection): the approval gate fires before any handler starts, independent of model intent (security-operations §6). - Idempotency-key cross-principal collision / discovery (
crossPrincipalAdmissionScopingconformance case plus existinglookupByIdAndKey/resolveAdmissionAuthority): keys are principal-scoped in every adapter's admission key and UNIQUE constraint. - Forged epoch / receipt and approval replay: covered by the existing ledger conformance cases (
settlementConflicts,abortIdempotency,approvalDecisionIdempotency,childReservationFencing) and the S2 fabricated-child IDOR pin (travel-planner-subagents-durable.test.ts). - Live-model smoke transcript redaction (
examples/providers/test/live-smoke.test.ts): everyRunEventis Schema-encoded and passed through the structuralRedactorbefore it can appear in an assertion message; the credential isConfig.redacted; the live block is gated off ordinary runs byphase7LiveProfileEnabled.