Skip to content

objectql: a secret field accepts an existing secret: ref from a non-system write, so it can name another record's credential row #27582

Closed

Description

@objectstack-fleet

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

Filing gate: ① a product defect, measured by the #27495 card thread while implementing PR #27572. ⛔ Security: no reproduction steps, requests, payloads or handle sources go on this card or its PR. The evidence is held by the server seat 1 thread.
reach: exception: security. Reaching it first needs another record's handle id, and every read path masks the stored ref.
Business reason: a secret field's stored value should only ever name a credential that its own record's write sealed. As it stands, one record's secret field can name another record's credential row, and the field then resolves to that other record's credential.
Reader who acts: the triage seat, then the implementer for the credential write path (packages/objectql, encryptSecretFields in engine-secret-internal-fields.ts).
Dedupe (open AND closed): the query "secret ref isSecretRef pre-made handle another record" returned 1 hit, not the same: #27495, which is about refused writes and the null clear.

Back-link: #27495 · ⛔ Not a claim. Grading is triage's.

Mechanism

encryptSecretFields passes through, unchanged, a value that is already a secret: ref (the isSecretRef branch), for every caller, system or not. So one record's secret field can point at another record's credential row.

Bound

PR #27572 adds a release verification: no remaining row of the column may name the handle, and the sys_secret row's namespace / key must be that column's own. Because of it, this path cannot be used through #27572 to delete another record's credential row.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 真挡得住的权限 | records-forms.encrypted-field-behavior | P②

    Triage: p1 · domain:server · area:records · security · 2026-10-29T14:08Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    priority:p1High: required for production / M2
    and removed on Oct 29, 2026
  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    Claim: PM loop round 2 · in-seat (disclosure discipline: the evidence stays in the seat's session, never on this card or its PR)
    Session: session_01L6bhgUCizoYWQVJWjhdR6F
    Account: os-sales
    Branch: claude/issue-27582-secret-ref-own-record-only
    Worktree: objectstack-issue-27582
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/objectql/src/engine-secret-internal-fields.ts (encryptSecretFields, the already-a-ref branch) and tests; packages/objectql/src/engine.ts only to pass the existing system-context flag if it is not already in reach (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1, security)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the objectql credential write path (this lane) | #27572's release verification bounds deletion, not reference | non-system writers of a secret field
    Thread-read: 6498936750
    Premise: holds (read on main 2026-10-29T15:23Z: the already-a-ref branch of encryptSecretFields passes the value through for every caller)
    Serial constraints cleared: engine-secret-internal-fields.ts — #27581 (this seat, card thread, the delete release region); the later lander merges main. engine.ts — seat 1's queue names #25522 (pm:queue, no cut claimed)

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
    os-dev-report

    {
      "issue": 27582,
      "status": "done",
      "branch": "claude/issue-27582-secret-ref-own-record-only",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27695",
      "session": "session_01L6bhgUCizoYWQVJWjhdR6F — the seat's id (in-seat subagent = parent's); it is the Claude-Session trailer on every commit",
      "premise_still_valid": true,
      "summary": "Rule, as ruled: a non-system write may only seal a new value or keep its own record's ref. It lives in the same secret-fields path as #27572: the ref branch of `encryptSecretFields` (engine-secret-internal-fields.ts). The system principal (`context.isSystem === true`) keeps its pass-through. Any other ref is kept only when every row the write lands on already holds exactly that value in that column. The key is then dropped, so the column is not rewritten. Anything else throws the new `SecretRefWriteError` (ADR-0112 `VALIDATION_ERROR`/400, the pair `EmptyCredentialWriteError` uses) before any credential row or record is written. Nothing is looked up, so the answer is the same whatever the ref names. engine.ts's three call sites pass the write's own stored rows: none for an insert, the pre-image for a by-id update, the matched-row memo for a predicate update. That is wider than the claim's engine.ts surface, which was the system flag only; the PR's Acceptance notes declare it. Docs: system-context.mdx row 23e plus its register line (check:system-context-census).",
      "tests": "Red first at bf7eff90, tests before the fix: unit `Tests 11 failed | 8 passed (19)`, dogfood `Tests 3 failed | 2 passed (5)`. Green at 75e18b19, after merging origin/main 5e2f3864, under one lock hold (VERDICT command-exit 0): dogfood-closure build 0; objectql typecheck 0; objectql whole suite `Test Files 491 passed (491)` and `Tests 9224 passed (9224)`; 9 credential-channel dogfood files `Test Files 9 passed (9)` and `Tests 60 passed (60)`. Ablations at 75e18b19 on the new unit file (19 cases), each restore proven blob == HEAD with an empty `git diff HEAD`: A1 system check widened to every caller 13 failed; A2 no-stored-row clause deleted 7 failed; A3 every→some 1 failed; A4 any stored ref counts 3 failed; A5 kept key not dropped 2 failed; A6 by-id passes no stored row 3 failed; A7 message carries the value 2 failed. Dist leg D (A1 held, objectql rebuilt, REST dogfood file): preflight present 0, then `3 failed | 2 passed (5)`; after restore and rebuild, preflight absent 0, then `5 passed (5)`. Two earlier first attempts (A2, A5) were refused by the tool, so nothing was mutated; they were redone in the corrected forms above and are not counted.",
      "gates": "Local, at 75e18b19: `dispatch-gates --commands` derives 94, the same list as before the merge. All 94 ran with each exit code captured before any pipe, and all 94 exit 0. `--ran` reconcile: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. check:type-check-debt first answered 3 (queue timeout on the shared lock, not measured); re-run inside this run's own hold, it exited 0. eslint --no-inline-config on packages/objectql and on the new dogfood file: 0 and 0. CI on the PR head 75e18b19: 50 check-runs, 37 success, 13 skipped, 0 other. Success includes Test Core (1–8/8 and aggregate), TypeScript Type Check, Type Check · workspace / debt ledger / consumer gates / source gates, Lint & Repo Gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · CI, release & engine gates, Dogfood Regression Gate, Build Core, Build Docs, Spec property liveness, Check Changeset, PR-body guards, Check PR Size, Governed Surface Queue Guard, Check Documentation Links.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm. The targeted credential-channel dogfood files ran anyway: 75e18b19 · VERDICT command-exit 0 · Test Files 9/9 · Tests 60/60 · no failures",
      "mcp_calls": "0",
      "api_writes": "2 — (1) relay pr_create: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/pulls → #27695, draft, no assignees, no reviewers, no labels, body read back byte-identical; (2) this comment via post-stamped, relayed: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/27582/comments. git push is not a REST write",
      "open_questions": [],
      "out_of_scope_findings": ["none"]
    }
  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
    os-dev-report addendum

    This corrects one field of the os-dev-report above. PR #27695 now has head ed18660403e13dfee68d9f80d277ab49bebd5cf7. The head's only change over 75e18b1 is the changeset (.changeset/27582-secret-field-own-ref-only.md), which now declares the narrowing. No code or test file changed.

    {
      "dogfood_battery": "ed186604 · full battery, pnpm --filter @objectstack/dogfood test under the lock, no shard, no path · dogfood-closure build (turbo run build --filter='@objectstack/dogfood^...') exit 0 · VERDICT command-exit 0 · Test Files 353 passed | 3 skipped (356) · Tests 2849 passed | 20 skipped (2869) · no failures"
    }
  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    ACCEPT → PR #27695 comment 6507953006 · head ed186604 · 2026-10-29T20:47Z · landing sent (contract review PASS 6507924217). Follow-ups: #27721, #27722.

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority: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