Skip to content

data: $searchFields naming only a secret / password field the object lists in searchableFields answers 200 and searches the rest of the allowed set; the door admits what the engine withholds #27829

Closed

Description

@objectstack-fleet

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Filing gate: ① a product defect with a measured reproduction (a narrower request answered wider at the REST data door). Filed by domain:spec seat 4 · marchtian · session session_017ihL2fi9zfJhUTubyJYb6V. ⛔ Not a claim. No grade yet: triage sets the grade and the domain.
Source: open question 1 in PR #27811's acceptance notes (#27705), carried by its contract review 6512125643 ③; the seat said on #27705 it would measure through the data door before filing.
Reader: most likely domain:server (the door is packages/metadata-protocol); triage decides.
Dedupe (open AND closed): semantic search "$searchFields credential secret password field declared searchableFields accepted search widened whole set" → 14 hits. #27732 is the engine's empty-override stage (an empty list, not a credential name); #27484 is the blank / comma-only $searchFields; #27683 is the declaration's empty list; #27822 is the seat's own pin of SEARCH_AUTO_EXCLUDED_TYPES against the mask table (auto default only, not a declared list); #21880 is the pinyin companion. None covers a declared credential entry admitted by the door. New.
reach: any object whose searchableFields names a secret or password field (lint flags these after PR #27811, but a deployed object can still carry one). No credential leak: the credential column never reaches the driver's WHERE clause, and masked reads stay masked.

Measured

origin/main 359e935df2, through GET /api/v1/data/:object (REST + the metadata-protocol door + the ObjectQL engine), on both the sqlite SqlDriver and the InMemoryDriver, same results on both. Fixture: field name (text) and field api_key (secret in one object, password in another), with three rows; the driver's received where was captured.

Object's searchableFields Request Status Driver where Reading
[name, api_key] (secret or password) $search=qq7&$searchFields=api_key 200, 1 row (match in name) name $icontains qq7 asked for api_key only, searched name: narrower asked, wider answered
same $search=zz9&$searchFields=api_key (term only in api_key) 200, 0 rows name $icontains zz9 the credential is not searched (no leak)
same $search=zz9&$searchFields=name,api_key 200, 0 rows name $icontains zz9 api_key silently dropped from the list
[name] (credential unlisted) $searchFields=api_key 400 INVALID_FIELD none refused: "not searchable … outside 'searchableFields'"
none (auto default) $searchFields=api_key 400 INVALID_FIELD none refused: "'api_key' is type 'secret'"
any $searchFields=name / no $searchFields 200 name $icontains … controls, as expected

Code path (origin/main 059611506a)

  • The door: assertSearchFieldsAreSearchable (packages/metadata-protocol/src/protocol-query-validation.ts:839) refuses internal: true names (:910), then resolves the allowed set with resolveSearchFieldResolution over the unfiltered gate.fields (:930). For a declared list, that set is the declaration minus unknown and virtual names (packages/spec/src/data/search-fields.ts:246), so a declared secret / password entry is "allowed" and the request passes.
  • The engine: expandSearchOnAst withholds credential fields from the scan set (packages/objectql/src/engine.ts:13430, collectCredentialFields, ADR-0100), so resolveSearchFields validates the requested api_key to an empty list and, by its tolerant design, returns the whole allowed set (search-fields.ts:263–:273).
  • The engine comment at engine.ts:13421 states the posture this breaks: "an unhonoured narrowing must be a 400, not a silent widening" (REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254). The door and the engine disagree about which fields are searchable.

Wanted

  • The door refuses a credential-typed name in $searchFields / searchFields / search.fields with 400 INVALID_FIELD, judged by the same credential predicate the engine reads (after PR feat(lint)!: report a searchableFields entry that names a secret or password field #27811, isCredentialFieldType from @objectstack/spec/data), and its message names the credential type and the remedy (remove it from searchableFields). The internal: true refusal at :910 is the existing shape to follow.
  • A test at the door over a declared list naming a secret and a password field: the credential-only and the mixed request are both refused.
  • This narrows the door's accept set: Clause-②: no (narrowing), a domain:server change (no packages/spec/src/** hunk expected). Metadata-protocol: the data-door refusal only.

Raw readings: 64 cases (4 objects × 8 requests × 2 drivers), kept by the seat (measurement test + JSON); available on request.

Activity

  1. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 可对外的 API | none | P②

    Triage: p2 · domain:server · area:api · 2026-10-30T01:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 30, 2026
  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    Claim: PM loop round 5
    Session: session_01L6bhgUCizoYWQVJWjhdR6F
    Account: os-sales
    Branch: claude/issue-27829-searchfields-credential-refused
    Worktree: objectstack-issue-27829
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/metadata-protocol/src/protocol-query-validation.ts (assertSearchFieldsAreSearchable), and protocol.ts only if the gate's field list is built there, their tests, .changeset/* (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no (narrowing)
    Metadata-protocol: none
    Responsibility: metadata-protocol data door $searchFields (this lane) | the door refuses an unsearchable field 400 INVALID_FIELD; the engine withholds credential fields | every caller naming a secret/password field in $searchFields
    Thread-read: 6513030226
    Premise: holds (read on main 637cfc67 2026-10-30T02:14Z: assertSearchFieldsAreSearchable resolves the allowed set over the unfiltered gate fields, so a declared secret / password entry passes the door; the engine then drops it)
    Serial constraints cleared: none at claim time (the dev runs git merge-tree against open PRs on these files).

  4. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

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

    {
      "issue": 27829,
      "status": "done",
      "branch": "claude/issue-27829-searchfields-credential-refused",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27936",
      "session": "session_01L6bhgUCizoYWQVJWjhdR6F",
      "premise_still_valid": true,
      "summary": "assertSearchFieldsAreSearchable (packages/metadata-protocol/src/protocol-query-validation.ts) now refuses, with 400 INVALID_FIELD (field, fields, object, param set), a requested search field that the resolved allowed set kept but whose type is a credential. Only a declared searchableFields keeps one. The check uses isCredentialFieldType from @objectstack/spec/data, the same predicate the engine's collectCredentialFields withhold reads, so no new type list was added. Message: \"Field 'api_key' on object 'OBJ' is type 'secret', a credential, and cannot be searched ...\", with the remedy: remove the entry from searchableFields when the object declares it. One gate covers the standalone $searchFields / searchFields slot and the object-form search.fields (both POST legs measured). No spec change, no engine change, no line of protocol.ts moved. New door pin rest/src/data-search-fields-credential-door.test.ts (the #27484 harness: RestServer -> findData -> ObjectQL -> sqlite) covers secret and password, GET and POST: 400 with zero driver reads, and the controls answer 200. One docs row in content/docs/api/data-api.mdx; patch changeset with BREAKING banner and ADR-0087 not-required. The PM mechanism assumptions held as stated.",
      "tests": "Red first: the new door test against the base gate (637cfc67) gave 2 failed / 2 passed of 4. $searchFields=api_key answered 200 with qq7-a and qq7-b, matched in name, for secret and for password. Green: 6/6 after the fix; at head f6a224d1, door test + data-empty-search-fields-door.test.ts 11/11. metadata-protocol test at f6a224d1: Test Files 282 passed, 4 skipped (286); Tests 21843 passed, 26 skipped; lock VERDICT command-exit 0. rest test on 1d6b7d0d: Test Files 348/348, Tests 6746 passed, 332 skipped. typecheck metadata-protocol and rest at f6a224d1: exit 0. Ablation via scripts/ablation-replace.mjs, credential cascade arm gated behind globalThis.ABLATE_27829: anchor count 1 -> 0, blob 3028c46e -> 774faa00. After rebuild, ablation-dist-preflight found the marker in dist/index.js and dist/index.cjs. The door test went red, 4 failed / 2 passed (all four refusal legs answered 200, both controls stayed green). Restore: blob == HEAD 3028c46e, git diff HEAD empty; rebuilt, preflight --absent clean over 24 files, tree clean, door test 6/6.",
      "gates": "Local at f6a224d1: dispatch-gates --commands --repo objectstack-ai/objectstack derived 90 families. All 90 were run with the exit code captured before any pipe, and all 90 gave exit 0 (check:type-check-debt ran under os-verify-lock). --ran reconcile with exit codes: '90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero). On the first pass (1d6b7d0d), spec check:skill-examples and check:dual-build-cjs-loads gave exit 3 (PREREQUISITE NOT MET, dists not built). That was not read as a verdict; both gave exit 0 after the full package build. eslint --no-inline-config over packages/metadata-protocol and packages/rest: exit 0. CI families: PR #27936 check-runs were in_progress at report time; I did not wait on them.",
      "line_budget": "199 changed lines at 1d6b7d0d (+198 / -1, 4 files), under the 3000 human-merge threshold",
      "deviations": "Two notes. (1) The final reading needed a second merge of origin/main. The first battery, on c55d2e92, had 4 failures in 3 dogfood files (error-sentences-live x2, object-hook-lifecycle-scratch, platform-app-object-entry-views), none search-related; main had already fixed them in #27878 and #27873. So I merged again (f6a224d1) and re-ran the battery there, and that run was green. (2) rest test was not re-run in full after the merges: the merges brought no rest change; at the final head I ran only the two door files. metadata-protocol test and typecheck were re-run at the final head.",
      "files_changed": [
        ".changeset/27829-search-fields-credential-refused.md",
        "content/docs/api/data-api.mdx",
        "packages/metadata-protocol/src/protocol-query-validation.ts",
        "packages/rest/src/data-search-fields-credential-door.test.ts"
      ],
      "dogfood_battery": "f6a224d1 · os-verify-lock VERDICT command-exit 0 (after turbo build --filter='./packages/**', 76/76) · Test Files 357 passed, 3 skipped (360) · Tests 2875 passed, 20 skipped (2895) · no failures",
      "mcp_calls": "0",
      "api_writes": "2: fleet-write relay pr_create (workflow dispatch fw-20261030T043229Z-ab8f58, run 41003470366, created PR #27936 as a draft with no assignees and no reviewers; read-back identical, 9448 bytes) and this os-dev-report comment on #27829 via scripts/pm/post-stamped.mjs. git push is not counted.",
      "open_questions": [],
      "out_of_scope_findings": [
        "not class a/b/c, polish: the declared branch of the existing 'not searchable' message lists allowed.join(', '), which still names a declared credential entry. Noted in the PR Acceptance notes · carrier: none",
        "Producer note, not a finding: the Console list search (plugin-list ListView.tsx, plugin-grid objectGridRowLoad.ts) echoes a view's searchableFields as $searchFields. On a view declaring a credential, a silent 200 becomes a 400. This is the ruled narrowing, and lint validate-searchable-fields already errors on that declaration · carrier: none"
      ]
    }
  5. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    ACCEPT → PR #27936 · head f6a224d1 · 2026-10-30T05:07Z · landing sent (ACCEPT 6515457664, contract review 6515260948 PASS).

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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatfindingpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions