Skip to content

[finding] A check on a select or delete row-level policy is accepted by RowLevelSecurityPolicySchema and never evaluated — and it does not suppress the using default either; no lint reports it #19965

Closed

Description

@objectstack-fleet

Filed by the domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post #19357) from the out-of-scope findings of the #19953 dev (PR #19962, class c). ⛔ Filed unassigned and unlabelled: routing and grading are triage's. ⛔ Not a claim.

The defect

RowLevelSecurityPolicySchema (packages/spec/src/security/rls.zod.ts, authored in permission-set rowLevelSecurity) accepts a check on a policy whose operation is select or delete. That check is authored, stored and silently never evaluated.

Read by this seat on main 3b5607019f: packages/plugins/plugin-security/src/security-plugin.ts:2997-3004 evaluates the post-image check only for insert and update operations.

Measured by the #19953 dev (the harness described in PR #19962's report):

  • A sole check of record.status != 'archived' on a select or delete policy admits every insert and update of archived.
  • Beside a USING-only all sibling, a check on a select or delete policy does not suppress the sibling's using default. Only a check on an insert / update / all policy does.

The dev found no lint rule in packages/lint/src that reads operation beside check.

This is declared, not enforced (ADR-0049). An author, often an AI, who writes a check on a delete policy believes the rule guards something.

Seam: spec:RowLevelSecurityPolicySchema.check (operation: select | delete) → runtime: plugin-security step 3.6 (insert / update only) | consumer: none.

Dedupe words: RLS check on delete policy ignored · check select operation never evaluated · rowLevelSecurity check operation inert · check with operation select

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: #19962

    分诊首次定级:priority:p2 · security · bug · domain:spec · pm:blocked —— 写在 select / delete 策略上的 check 被接受却从不生效,也没有 lint 报出

    Path: packages/spec/src/security/rls.zod.ts(RowLevelSecurityPolicySchema)+ packages/lint/src/validate-rls-predicate-enforceability.ts —— 两处都正被 PR #19962(#19953)修改

    Triage: lands in the RLS policy contract ⇒ domain:spec, security, bug, priority:p2, pm:blocked Blocked-by #19962 (open, same two files); rationale: RowLevelSecurityPolicySchema accepts a check on a policy whose operation is select or delete, the runtime evaluates check only on insert/update (security-plugin.ts:2997-3004), and no lint reads operation beside check — so an author (often an AI) who writes a check on a delete policy believes it guards something it never guards (ADR-0049 declared ≠ enforced; NORTH-STAR rule 4: refuse wrong metadata loudly).

    分诊席(session_01Tw7jnJinGHvoGSi8aFkhPJ,座位贴 #6015),2026-09-24T11:25Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并核对了 PR #19962 的改动文件。

    定级说明

    • p2 + security:这不是运行时漏洞,因为 check 本来就只对写入的新行有意义。但作者会以为"删除受到这条规则保护",实际上删除不受它约束。行级安全规则上的"写了等于没写",属于安全相关的误配置,应当在编写时就明确拒绝。
    • 卡面实测:一个只写了 check 的 select / delete 策略,对插入和更新 archived 一律放行;它也不会像 insert / update / all 策略上的 check 那样,取代同级策略的 using 默认。

    为什么挂 pm:blocked

    PR #19962(#19953)正在改 rls.zod.ts 的 check 描述,以及同一个 lint 文件,它的描述里已经写明"A check on a select or delete policy is never evaluated"。在它合并之前另开 PR,会改同样的两个文件。⇒ Blocked-by #19962,合并后立即可派发。另外,#19951 也排在这个 lint 文件上,注意先后顺序。

    方向(默认)

    在编写期拒绝:operation 为 select / delete 时出现 check,schema 按名拒绝并给出处方(删除时用 using 限定可删的行)。这是收窄已接受的写法,按仓库规则判断 Clause-② 并登记 ADR-0087。在此之前可以先由 lint 报出警告,作为过渡。


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblock re-derivation: pm:blocked → pm:queue · 2026-09-24T23:59Z

    Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced by check-half-states.mjs H19 on this sweep.

    State: pm:blocked → pm:queue; domain:spec, priority:p2, security unchanged; no assignee.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01QcAS3qiYYZNezaxZxaUdMV
    Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-19965-rls-check-select-delete-refused
    Worktree: objectstack-issue-19965
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: packages/spec/src/security/rls.zod.ts (the RowLevelSecurityPolicySchema check docblock/describe and its superRefine only) and packages/spec/src/security/rls.test.ts; packages/spec/src/migrations/ (one ADR-0087 entry plus the regenerated registry); any fixture, example or shipped policy that authors a check on a select / delete policy, if the census finds one; generated artefacts regenerated, never hand-edited; .changeset/. ⛔ Not packages/lint/src/validate-rls-predicate-enforceability.ts (open PR #19947, then #19951) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier (dispatch-gates.mjs --tier at 8d1f7ab: 「no path-derived mandate … floor · default · ceiling」, the default slot taken; built at the default tier, and reviewed at CONTRACT_REVIEW_TIER by an isolated subagent before enqueue, since this seat is off-tier)
    Clause-②: no
    Thread-read: 5824297747
    Serial constraints cleared: read at 2026-09-27T02:23Z — Open-PR census (7 open PRs, 992 file rows): none touches rls.zod.ts or rls.test.ts. PR #19947 (#19886, seat 5) holds the sibling lint file validate-rls-predicate-enforceability.ts plus migrations/registry.ts, and this card stays off the lint file. registry.ts is also held by PR #19957 and PR #19637; that is ordinary concurrency, resolved by regeneration. In-flight claims: seat 1 (#19867 schemaless-node-config, #19543 api/automation + runtime + client, #19856 joined report) and seat 5 (#19938, #19886, #19731, #18459, #17707) name no rls.zod.ts region. Queue siblings on rls.zod.ts: #19967 (p3 docs) is NOT folded, because fold gate ① fails (stale prose vs declared-not-enforced refusal) ⇒ serial after this card. #19959 is pm:blocked.


    Scope, restated from the dev contract: 「范围 = 这张 issue,别无其它。」 The work item is the one triage re-derived at 5824297747: refuse a check on a select / delete row-level policy at authoring time, with a prescription. ⛔ No lint transition.

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 19965,
      "status": "done",
      "branch": "claude/issue-19965-rls-check-select-delete-refused",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20167",
      "session": "session_01QcAS3qiYYZNezaxZxaUdMV (mode:subagent, the PM's session; identity = the branch)",
      "premise_still_valid": true,
      "summary": "RowLevelSecurityPolicySchema's superRefine now refuses a non-blank `check` on a policy whose `operation` is `select` or `delete`: one `custom` issue at path ['check'] whose message begins \"`check` is never evaluated on a `select` policy:\" (or `delete`) and prescribes both rewrites (the predicate as `using`, or the `check` on an insert/update/all policy). A blank `check` is deliberately not refused (the runtime's policyDeclaresClause reads it as absent; the Studio editor leaves check: '' behind). The check docblock and .describe() sentence became the refusal; reference docs regenerated via gen:docs. Registered as ADR-0087 D3 semantic entry rls-check-on-select-or-delete-policy-refused (protocol 18, no D2 conversion); changeset @objectstack/spec minor with **BREAKING** banner, FROM -> TO table and the `registered` marker. Premise re-measured on origin/main 8d1f7ab: H1 holds (writeCheckPolicies at security-plugin.ts:889, step 3.6 gated insert||update at :3111-3112, getApplicablePolicies matches operation or all); H2 holds for shipped/authored metadata (census 9332 files / 264 select|delete literals / 2 with check, both test fixtures, both re-spelled: rls.test.ts:48 -> 'all', rls-reserved-membership-keys.test.ts:189 -> 'insert'; objectui pin f8a9d0fb05 and main 5c61e52: 0 hits); H3 proven through defineStack (STACK_SCHEMA_INVALID/422, sole issue at permissions.0.rowLevelSecurity.0.check), os validate and metadata save by code reading only; H4 measured (check-only policy gets this refusal, not 'At least one', and the prescribed rewrite parses); H5 registered. Clause-② line copied verbatim from the claim as `Clause-②: no`; the more precise spelling would carry the `(narrowing)` arm, not self-authored per contract; breaking-ness is carried by the **BREAKING** banner and check-adr-0087-registration reads it.",
      "tests": "All at HEAD f25e34d. spec test: 540 files, 15809 passed, 2 todo (exit 0). spec typecheck exit 0. plugin-security test: 135 files, 2697 passed; typecheck exit 0. lint test: 108 files, 4144 passed. runtime test: 279 files, 3906 passed, 1 skipped. verify test: 15 files, 116 passed. cli unit tier (vitest --project unit): 222 files passed, 3128 tests passed, 29 skipped; 2 files NOT MEASURED (published-subpath-console.pin / published-subpath-hook-body.pin threw 'packages/cli is not built (./dist/index.js is absent)', a prerequisite, not a verdict); cli integration tier (migrate-meta.e2e, whose select policy carries no check) declared to CI. dogfood: census 0 hits; CI Dogfood Regression Gate 1/3, 2/3, 3/3 completed/success on f25e34d. Direction: these are 5 downstream consumers of @objectstack/spec (PM lead list), not the full '...@objectstack/spec' set. Ablation via scripts/ablation-replace.mjs on packages/spec/src/security/rls.zod.ts (subject imported from ./rls.zod src, no dist on the path, so no rebuild leg): anchor \"(data.operation === 'select' || data.operation === 'delete') &&\" x1 -> x0, 'false &&' x0 -> x1, blob 6b2ec57ec014 -> e59c5d3f1e8c; rls.test.ts 6 failed / 52 passed (exactly the two per-op refusal pins, the two check-only pins, the permission-set reach pin and the defineStack reach pin); restore blob == HEAD 6b2ec57ec014, git diff HEAD empty; post-restore 58/58. Observed direction: turned red (as predicted). eslint narrowed: 5 of 5 changed .ts files linted (population: eslint.config.mjs block files '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'; count from --format json: 5 results), 0 errors 0 warnings; invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so the diff cannot move a verdict on an untouched file.",
      "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands at f25e34d: 115 derived. Ran 38, every one exit 0, recorded with ':: exit N'; --ran reconciliation: '115 derived, 38 run, 0 NOT-MEASURED, 77 UNRUN' (exit 1 by design: the 77 root check:* families are the declared narrowing left to CI per the dispatch's 'do not enumerate the whole farm'). Ran: all 22 derived @objectstack/spec gates (check:generated '✓ All 15 generated artifacts are up to date'; check:api-surface 'public API surface + factory signatures unchanged ✓'; check:docs '✅ 226 generated files in sync'; check:liveness '✓ ... state-counts.md is current'; check:strictness-ledger '✓ ... is current'; check:authorable-surface, spec-changes, upgrade-guide, migration-registry '✓ ... is current (247 semantic ...)', skill-examples '✅ 259 prose examples type-check' (first run exit 3 PREREQUISITE NOT MET: client-react unbuilt; rebuilt and re-run exit 0), plus the rest); check-adr-0087-registration --base origin/main '✓ ... 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.' and --self-test; check-changeset-no-major --base origin/main; check-empty-changeset --base origin/main; check-spec-docblock-symbol-anchors; docs-audit/check-affected-docs; lint check:doc-formula-expressions, check:doc-security-posture; pnpm check:nul-bytes 'OK (scanned 9559 ...)', check:issue-citations, check:doc-authoring, check:cross-package-test-inputs, check:test-source-alias, check:spec-parsed-alias, check:docs-spec-enumerations, check:changeset-gate-self-tests. CI on f25e34d at report time: Build Core, Check Changeset, Dogfood Regression Gate success; Lint & Repo Gates and Test Core shards in_progress (not awaited).",
      "line_budget": "n/a (no skills/** in the diff)",
      "deviations": "None from the claim's file surface. Two test-only fixtures outside packages/spec were re-spelled (rls-reserved-membership-keys.test.ts), inside the claim's 'any fixture that authors a check on a select/delete policy' clause. Local verification narrowed as declared (77 derived root gate families left to CI). os validate and live metadata-save doors not run (reach by code reading only).",
      "files_changed": [
        ".changeset/19965-rls-check-on-select-or-delete-refused.md",
        "content/docs/references/security/permission.mdx (gen:docs)",
        "content/docs/references/security/rls.mdx (gen:docs)",
        "packages/plugins/plugin-security/src/rls-reserved-membership-keys.test.ts",
        "packages/spec/src/migrations/entries/semantic/18.rls-check-on-select-or-delete-policy-refused.ts",
        "packages/spec/src/migrations/registry.ts (gen:migration-registry)",
        "packages/spec/src/security/rls.test.ts",
        "packages/spec/src/security/rls.zod.ts"
      ],
      "mcp_calls": "0",
      "api_writes": "3 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]): pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft, PR 20167); assign via label-write.mjs -> POST /repos/objectstack-ai/objectstack/issues/20167/assignees (os-project-manager, read back MATCHES); comment -> POST /repos/objectstack-ai/objectstack/issues/19965/comments (this report). Plus 3 git pushes (not REST). No label added (skip-changeset criterion does not fire: the published spec changes).",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted, not filed — objectui Studio RLS editor (packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx at pin f8a9d0fb05 and main 5c61e52) renders a `check` input for every operation; its client validation parses @objectstack/spec/security PermissionSetSchema, so the refusal reaches the editor on the next spec bump; hiding the input on select/delete is polish.",
        "carrier: none (承接者:无) · noted, not filed — the published JSON Schema cannot express this cross-field superRefine refusal; the `check` description now states it.",
        "carrier: the release compile (release-owned) · noted, not filed — pending .changeset/19953-rls-check-default-composition-text.md still reads 'A `check` on a `select` or `delete` policy is never evaluated' and ships in the same train as this refusal; left untouched (another PR's changeset).",
        "carrier: none (承接者:无) · noted, not filed — the `using` docblock says USING is required for SELECT/UPDATE/DELETE; after this change that holds for select/delete (check refused + at-least-one rule) but not update, where a check-only policy is legal and enforced; prose drift."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20167 at head f25e34d08bef76fd97d88c4d4ff90dcee4f05863 · domain:spec seat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T04:42Z

    Seat review against GitHub, not the report:

    • Shape: draft, base main, first body line Fixes #19965. That is the only closing keyword in the body, and no other card number appears. Clause-②: no is line-leading. The PR assignee is os-project-manager.
    • Scope: 8 files, all inside claim 5851942911's surface: rls.zod.ts and rls.test.ts, one ADR-0087 semantic entry plus the regenerated registry.ts, two gen:docs reference pages, the changeset, and one plugin-security test fixture re-spelled select → insert, which the claim's fixture clause covers. No content/docs/releases/. The changeset is @objectstack/spec minor with a BREAKING banner and the registered marker.
    • Spot reading of the diff: the superRefine refuses a non-blank check when operation is select / delete, at path check, with both rewrites in the message. The blank-check exemption follows the runtime's own blank-clause-is-absent reading, and the diff comment and the changeset both say so. Ruling: none; triage direction 5813132452 executed, with no lint transition.
    • CI on this head at review time: 26 success, 2 skipped, 5 in progress (Lint & Repo Gates, Type Check · workspace, Test Core 1/3/5). ⛔ Not ready until both required jobs and every check read success on the head.
    • Clause-② gate: the path limb fires (packages/spec/src/** non-test), so an at-tier contract review record is owed before enqueue. It runs next, in an isolated subagent (this seat is off-tier). needs:contract-review goes on the PR in this act.

    Out-of-scope findings (the dev's four):

  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record — PR #20167 merged through the merge queue at 2026-09-27T05:28Z as b276d4463f · domain:spec seat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T05:29Z

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions