Skip to content

Turn the security invariants into guard tests that fail when a new route, tool, or connector forgets them #306

Closed

Description

@serge-ivo

Every security invariant in this Worker is enforced at one correct choke point, and every one of them has at some point been quietly bypassed by the next thing added next to it. All of the following shipped looking right and were found by accident:

The pattern is always the same: a correct mechanism exists, and the seventeenth call site does not go through it. A reviewer cannot catch that by eye.

What to build

Guard tests in the shape this repo already uses — instance-config.test.ts ("no source file issues UPDATE agent_instances SET config = ?"), lib/import-graph.ts + its tests, scripts/check-*.mjs, store/console/src/lib/control-labels.ts. Walk the tree, fail with the offender list and the reason, and prefer a ratcheting budget over a hardcoded allowlist.

Invariants to guard (verify each is actually true before asserting it):

  1. Every authenticated route resolves identity through requireUser/requireAdmin — nothing verifies a session inline without applying the suspension gate.
  2. Every outbound fetch to a user-supplied host goes through safeFetch, with no bypass path, including test/probe endpoints.
  3. Every mutating registry tool passes the consent gate before the network or the vault is touched.
  4. Stored credentials go through the envelope helpers in lib/crypto.ts — no second scheme, no plaintext column.
  5. Untrusted remote content is fenced via lib/untrusted-fence.ts, and the fence neutralizes its own closing marker.
  6. Destructiveness is judged on the tool name WE send, never a server-supplied destructiveHint.

Each guard must be verified to go red when the invariant is broken.

Activity

  1. added
    backendBackend / Worker / API work
    securitySecurity hardening / audit finding
    on Aug 6, 2026
  2. serge-ivo commented on Aug 6, 2026

    @serge-ivo
    ContributorAuthor

    Shipped in ff372a1.

    Six guards in workers/api/src/lib/security-invariants.test.ts, over a pure lexer in lib/source-guard.ts (comments, string literals and regex literals blanked, line numbers preserved) with its own 23 unit tests:

    1. Suspension reaches every authenticated surface — a file that calls verifySession must also call isSuspended, with two documented non-authorizing exceptions (error attribution in index.ts, bucket keying in rate-limit.ts), each ratcheted so a stale exemption fails.
    2. The connector layer never bypasses safeFetch — no bare fetch( under lib/connectors/**, lib/steps.ts, lib/tools.ts, agent-do-knowledge.ts.
    3. Mutating registry tools pass the write-consent gate — a connector tool whose name reads as mutating is scope:"write"; a tool that lets the caller choose the HTTP method cannot be scope:"read"; a write-scoped tool names its connector.
    4. Credentials have one envelope scheme — the credential tables are derived from the migrations (a ciphertext column + a wrapped-DEK column), and every INSERT/UPDATE writer must import lib/crypto.ts; crypto.subtle.{wrapKey,unwrapKey,encrypt,decrypt} exists in exactly two named places.
    5. The untrusted fence is not re-implemented — the marker string lives only in lib/untrusted-fence.ts.
    6. Destructiveness is judged on our own name — no code reads destructiveHint/readOnlyHint/idempotentHint/openWorldHint, and isDestructiveToolName is still what the grant check calls.

    Mutation-tested: 13 mutations, one per assertion, each confirmed red then reverted. Two first drafts stayed green under mutation and were wrong — one tested for the isSuspended identifier (which the import line satisfies on its own), the other scanned comment-stripped code for SQL that only ever lives inside string literals and had been finding zero writers. Both now carry explicit non-vacuity assertions.

    Violations found and fixed here

    • routes/public.ts — BOTH authenticated routes (POST /v1/public/webhook/:instanceId/ingest, POST /v1/public/agents/:id/import-gdoc) verified sessions inline, so the [Admin] MCP repo/scaffold tools bypass the requireUser suspension gate #273 suspension fix never reached them. Now requireUser. Regression test added.
    • routes/agents.ts — same on GET /v1/agents/:id; auth is optional there so it applies isSuspended by hand, like the WS upgrade. Two regression tests added.
    • connectorClient.fetch() — its URL is the caller's, so it was a standing SSRF bypass carrying the owner's minted Bearer. Now safeFetch. Unused today, which is the only reason it was not already one.

    Violations found and filed, not fixed

    CI on ff372a1 passed every step through pnpm test (4211 tests). It failed on three console e2e specs that were already red three commits earlier — tracked as #309.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API worksecuritySecurity hardening / audit finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions