Skip to content

[security] Stored datasource credentials are served unredacted to an admin through a read path outside the two datasource read doors — detail withheld pending maintainer #21086

Description

@objectstack-fleet

Filing gate: ① a reproducible defect with a named landing site — the datasource credential redaction (packages/spec/src/data/datasource-credential-redaction.ts and the read paths that serve stored datasource metadata). reach: the public REST API, reproduced twice by the P0 run's runner (not yet independently re-derived — the dev's premise check is the second derivation). Reader: the maintainer, who holds the reproduction; dispatched directly at the maintainer's request. Dedupe: semantic search on datasource credentials served in cleartext through an admin data read (open + closed) → 5 hits, none this path; same family, all closed: #7990, #20590, #13405.

QA-source: #21056 · integration-system.datasource-credential-refusal-matrix · negative[0]

What is known publicly

  • Datasource credentials are declared write-only: the two datasource read doors redact them (the run's negative[0] passed on both).
  • A third, admin-only read path serves the stored datasource configuration WITHOUT that redaction, including credential values still present at rest (a still-writable key and legacy inline credentials). Members are refused there (403) and anonymous callers get 401, so the exposure is admin-only — it still contradicts the write-only contract.

Detail withheld pending maintainer (RUNNER rule 2). Maintainer ruling, verbatim (Claude Code session session_01MRdbfpy4sQT8bUjmMhxsN7, 2026-10-01):

只给我看的两条,你也直接派发处理

Landing site

The datasource credential redaction and the read path that bypasses it; the maintainer holds the reproduction and the mechanism.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01MRdbfpy4sQT8bUjmMhxsN7
    Account: os-bill
    Branch: claude/issue-21086-datasource-credential-read-redaction
    Worktree: objectstack-issue-21086
    Domain: domain:services (expected — dispatched at the maintainer's direction ahead of a triage grade; the landing sits in the datasource read paths)
    File surface: the datasource credential redaction (packages/spec/src/data/datasource-credential-redaction.ts, packages/metadata-protocol/src/metadata-redaction.ts) and the read path that bypasses it (exact file to be located by the dev and declared in the report); stop on breach
    Container & model: M, mode:subagent, tier default (security-boundary fix; dispatch-gates --tier: no path-derived mandate)
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: 34 in-flight pm:dispatched claims scanned, none declares the datasource redaction or read-gate files. Sibling #21087 is dispatched this round on an adjacent datasource read path: each dev declares its files before editing, and on any shared file the later one stops and reports.

    Dispatched at the maintainer's direction. Ruling verbatim (Claude Code session session_01MRdbfpy4sQT8bUjmMhxsN7, 2026-10-01): 「只给我看的两条,你也直接派发处理」. The reproduction goes to the dispatched dev in-session only. ⛔ It does not go into any GitHub text.

    Landing: a security/permission-boundary change, so the PR stays draft for the maintainer's review (human floor).


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: grade after dispatch — bug · security · priority:p1 · domain:services · area:access. ⛔ The claim, the assignee and the state are unchanged

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T06:02Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ It carries no detail beyond the card's own sanitized text.

    The card was dispatched at the maintainer's direction ahead of a grade. This act adds the grade only.


    Generated by Claude Code

  3. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    priority:p1High: required for production / M2
    on Oct 1, 2026
  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21086,
    "status": "done",
    "branch": "claude/issue-21086-datasource-credential-read-redaction",
    "pr": "#21115",
    "session": "session_01MRdbfpy4sQT8bUjmMhxsN7 — the parent session (this run is a subagent); the container CLAUDE_CODE_REMOTE_SESSION_ID resolves to it",
    "premise_still_valid": true,
    "summary": "Premise re-derived independently on a fresh showcase boot at base 94608a7. The read path is the generic data door, findData / getData, over sys_metadata. It also covers sys_metadata_history, which the card did not name. The metadata-type redactor was not applied there. Both datasource doors redacted correctly. The PM mechanism hypothesis holds. The fix makes those two methods serve a stored metadata body as its type's read projection, through the same kernel redactor registry and redactMetadataItem that every /meta exit uses, with no rule of their own. They read type when a projection names only metadata, then drop it. They return clean bodies by reference, so the stored bytes are kept, and they omit a body they cannot judge. A groupBy naming the body column on either table is refused 400 INVALID_FIELD, because a group key cannot be projected. That refusal narrows the accept set, so Clause-② is re-judged from the claim's 'no' to 'no (narrowing)', and the changeset is minor with the adr-0087 disposition not-required (no-migration-prescription). Declared file surface: packages/metadata-protocol/src/metadata-redaction.ts, packages/metadata-protocol/src/protocol.ts (findData and getData only), the new suite packages/metadata-protocol/src/protocol.data-door-stored-metadata-redaction.test.ts, .changeset/21086-data-door-stored-metadata-projection.md, and scripts/engine-double-contract.pinned.json. The last is outside the declared surface: it is one row that check:engine-double-contract demanded for the new suite's findOne double. The dispatch named protocol.ts as a file to stop at, so before editing it this run measured the sibling's pushed branch: claude/issue-21087-datasource-meta-read-gate at 611db07 touches only packages/rest (index.ts, meta-item-read-gate.ts, rest-server.ts and tests) and packages/runtime/src/domains/meta.ts. With no shared file, this run proceeded, re-checked the sibling's branch before pr_create (still disjoint) and touched none of its files. The PR is draft, assigned to os-bill, with no auto-merge, and its body was read back byte-identical (7406 bytes). Cleanup: node_modules removed and the worktree removed without --force. Every dev server this run started was stopped by its recorded PID. The scratch databases were deleted.",
    "tests": "All at HEAD a375e4e (git rev-parse --short HEAD), after merging origin/main fed0db8. (1) New suite, 17 passed. It pins both halves on both tables: list, by-id, select=metadata on both reads, history rows, an already-parsed body, a stand-in redactor showing the door follows the registry, stored-byte preservation of clean bodies, untouched other columns, objects and groupings, and the grouping refusal with code, status, param and field asserted and engine.aggregate not called. (2) @objectstack/metadata-protocol: vitest run gave Test Files 197 passed and 3 skipped (200), Tests 2948 passed and 19 skipped. typecheck (tsc --noEmit) exits 0, and tsc --listFiles includes the new suite. The VERDICT of every locked run was command-exit 0. (3) Ablations, one-time and not kept as tests, each run through scripts/ablation-replace.mjs in wrap mode. In every leg the anchor hit once, the blob changed, the restore matched HEAD and git diff HEAD was empty. They ran at d64f148; the subject is imported from src, so no build or dist hop was involved. A1 removed the list projection: 8 of 17 red, all on the withheld side. A2 removed the by-id projection: 2 red. A3 removed the grouping refusal: 3 red. P1 re-serialized clean bodies: the byte-preservation pin went red. P2 widened the scope to every object: the two outside-object pins went red. P3 never dropped the added type: the two projection pins went red. (4) Live, after the fix, on a showcase boot with credential-bearing datasource rows seeded through the API and by an at-rest sqlite update of legacy spellings: every list, by-id, select=metadata and query read of both tables served 0 seeded values. groupBy metadata answered 400 and groupBy type answered 200. A credential-free datasource row and a translation row were served byte-identical to their stored column on both tables. The datasource doors still answered 200 with 0 seeded values. (5) Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived on HEAD after a full turbo run build (72 of 72 tasks), gave 70 commands, all exit 0, each exit captured before any pipe. --ran reconciliation: '70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. An earlier run had three non-zero exits. The suite was fixed for two of them: check:engine-double-contract (the findOne double now routes through assertEngineFindOnePredicate, plus its ledger row) and check:where-matcher (the stub matcher now refuses combinators). The third, the plugin-teardown self-test, exited 3 on the shallow clone and went green after fetching its pinned fixture commit 621a487. check:dual-build-cjs-loads first reported PREREQUISITE NOT MET (missing dist) and went green after the full build. (6) Lint, a proven narrowing: eslint --no-inline-config --format json on the 3 changed TypeScript files reported 3 files, 0 errors and 0 warnings. --print-config resolves the config for each file. The config enables no type-aware linting (parserOptions is only ecmaVersion and sourceType), so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is declared to CI. (7) CI at report time, on a375e4e: 31 check runs, 9 success, 3 skipped, 19 in_progress, 0 failed. NOT MEASURED: the CI conclusion, which is in_progress.",
    "mcp_calls": "0",
    "api_writes": "3 — all through the fleet relay (scripts/pm/fleet-write/dispatch.mjs, one POST /repos/objectstack-ai/objectstack/dispatches each): ① pr_create → POST /repos/objectstack-ai/objectstack/pulls (#21115, draft; relay run 36830891383); ② label-write --assign os-bill → POST /repos//issues/21115/assignees (relay run 36830947244, read back: assignees os-bill); ③ this os-dev-report → POST /repos//issues/21086/comments. Plus git push of the branch (not REST).",
    "open_questions": [
    {
    "question": "A residual inference channel on the same door was measured in-session and is NOT closed by this PR. It serves no value, but it lets a stored credential be recovered from the stored body by probing. The mechanism and recipe are in the session-only file named in out_of_scope_findings, withheld here per RUNNER rule 2. Should the generic data door refuse it on sys_metadata / sys_metadata_history?",
    "options": [
    "A — the door refuses the query shapes that form the channel on these two tables: a further accept-set narrowing, Clause-② no (narrowing), in the same refusal family as this PR's grouping refusal",
    "B — record it as an accepted admin-only boundary and leave the door as it is"
    ],
    "recommendation": "A, as a position on the family closeout card below. The write-only contract says a stored credential is never recoverable from a read, and B leaves it recoverable by an admin. The narrowing would touch no shape that any shipped reader uses. The one objectui reader of this door, ObjectRefField's fallback, queries by type, reads name and label, and is unaffected by this PR, because the object type registers no redactor. It is a separate decision from this card's projection fix, so it is not folded in here."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: exception: security (could leak data). Measured in-session after this fix, on a fresh showcase boot as admin: a second, admin-reachable public read surface, outside the generic data door, still serves the same stored datasource credential material (11 distinct seeded values). The surface and the recipe are withheld per RUNNER rule 2 and are in the in-container file named in the carrier line below · evidence: post-fix measurement on a build of a375e4e · dedupe words: stored metadata body served raw · credential read surface outside the meta doors · stored datasource config second read surface",
    "class: a · reach: exception: security (could leak data). Measured in-session: a write path copies the stored metadata body, credential material included, into a second admin-readable store at write time. This is an at-rest cleartext copy, so a fix needs both the writer change and a migration of existing rows. The store and the recipe are withheld per RUNNER rule 2 (same file) · evidence: seeded values read back from that store after this fix · dedupe words: write-time copy of stored metadata body · cleartext credential copy at rest · stored datasource config copied on write",
    "family: the two findings above, the open question's inference channel and one unmeasured position belong to the #7990 / #20590 / #13405 family. Fold them into ONE family closeout card with an enumeration pin over every read surface that can serve or evaluate a stored metadata body, not into single-point cards. The unmeasured position is record-change events carrying the written row; it is a read-only inference, listed for that card to measure first, not a finding.",
    "carrier: the PM seat, through the in-container file /tmp/claude-0/-home-user/4f67a396-f4bd-55f0-a625-6a7c371668ad/scratchpad/issue-21086/session-only-detail.txt. It holds the exact requests, counts and landing files, is session-only and never for GitHub text, and is noted, not filed."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    PM review — PR #21115: ACCEPT; lands once CI is green (maintainer pre-authorized)

    Reviewed-by: PM seat, session_01MRdbfpy4sQT8bUjmMhxsN7. Implemented-by: os-dev subagent, same session.

    • Path surface (read from the PR file list): packages/metadata-protocol/src/metadata-redaction.ts, packages/metadata-protocol/src/protocol.ts (findData / getData only), the new suite protocol.data-door-stored-metadata-redaction.test.ts, the changeset, and one row in scripts/engine-double-contract.pinned.json. That row was demanded by check:engine-double-contract for the new suite's double, so it is in scope. There is no governed path, and the sibling [security] A datasource read path serves connection topology to signed-in members below the capability the datasource admin door requires — detail withheld pending maintainer #21087's branch was measured disjoint before editing.
    • Shape: one seam, reusing the /meta redactor registry through redactMetadataItem. It adds no second redaction dialect. Clean bodies are returned by reference, so the stored bytes are preserved.
    • Clause-②: no (narrowing): the grouping refusal on the body column narrows the accept set. The changeset is minor and BREAKING-flagged, with the ADR-0087 disposition not-required (no-migration-prescription), which is argued in the changeset.
    • Evidence accepted:
      • the new suite (17) pins both halves on both tables;
      • six ablations each turn red in the predicted direction and restore byte-identical;
      • @objectstack/metadata-protocol: 2948 tests pass, typecheck exits 0;
      • 70/70 derived gates exit 0;
      • the live post-fix boot serves 0 seeded values on every read of both tables.
    • Disclosure check: the PR title, body, changeset and report comment carry no reproduction.
    • Landing: permission/security boundary. The maintainer pre-authorized ready + queue once CI is green, selecting verbatim 「绿了就转 ready + 进队列(推荐)」 in Claude Code session session_01MRdbfpy4sQT8bUjmMhxsN7, 2026-10-01.
    • Out of scope: the dev reported further positions of the same family, outside this door. They go to ONE sanitized family closeout card, per the maintainer's ruling in the same session; that card will be linked here.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions