Skip to content

measure(plugin-email): who can list sys_email rows that carry identity mail (reset and verification links) — sys_email declares no requiredPermissions #24625

Closed

Description

@objectstack-fleet

Filing gate ②: a measured finding. It comes from the contract review of PR #24587 (step 3 of the 18.x line, card #24531), ③ item 5, and the dev's own observation in os-dev-report 6270006270.

  • Reader who acts: the epic seat (epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3).
  • Dedupe: none.

The finding. PR #24587 moves new identity mail (password reset, account invitation, verification, magic link, change-email) into sys_identity_email. That table sits behind requiredPermissions: ['manage_platform_settings'], a platform-scoped capability. But sys_email declares no requiredPermissions. Every identity mail an 18.0 install sent stays in sys_email until the 18.x ceremony moves it (#24581), and those rows carry reset and verification links. Nobody has measured whether a non-administrator, an organization administrator, or any principal with a CRUD grant on sys_email can list them through /data/sys_email or a view.

Wanted (measure first):

  • Under single, on a showcase with an 18.0-shaped database (test(dogfood): the 18.x upgrade harness — an 18.0 database boots on HEAD with its data (step 0) #24552's fixture holds a reset mail), list which principals can read sys_email rows through every door:
    • /data;
    • views;
    • export;
    • search;
    • activity mirrors.
  • Report whether a reset or verification link is readable by anyone who should not hold it.
  • If it is, propose the smallest fix and say what it closes. Candidate fixes: requiredPermissions on sys_email, an internal flag on the body fields, or redaction on read.

Priority p1 until measured, because a reset link is a credential.

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 12 · 2026-10-19T16:34Z
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: none. This is a measure-only stage: no commit and no PR.
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:services
    Seat: epic:#15194 (the epic lane)
    File surface: none written. The dev reads plugin-email, plugin-security, the /data door, views, export, search and the activity mirror, plus #24552's 18.0 fixture.
    Container & model: S, mode:cloud, model: TIER_DEFAULT
    Clause-②: no — measure only
    Responsibility: n/a — a measured finding (the #24587 contract review, item ③5)
    Thread-read: 6271215222
    Serial constraints cleared:

    • Read-only, so it meets no in-flight file.
  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Measured · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T22:39Z. The measure-only dev session (session_01VQqYtVmS5pxercvwxnkDWM) ran these probes over #24552's 18.0 fixture under single, on main 5e80009 (before #24587) and on #24587's head cd6fe0b4f. Its own write-up was not posted, so the seat records the result here. Credential values are deliberately left out: this card is public.

    Verdict: exposed. A principal with organization-administrator rights and without manage_platform_settings can read identity mail that already sits in sys_email, including the bodies that carry reset and invitation links.

    principal /data list / get / query export global search sys_activity mirror sys_audit_log mirror
    anonymous refused (401) refused refused refused refused
    member refused (403) refused no hit refused refused
    organization admin, no manage_platform_settings readable, with bodies refused (403) hit, with body text readable, with bodies readable, with bodies

    Since #24587 (on main as fbafb8533):

    • New identity mail goes to sys_identity_email. The same organization admin is refused there (403), and the mirrors and search return nothing for it.
    • What remains is (a) the identity rows already in sys_email from 18.0 / 18.0.0-next databases, and their activity and audit mirrors, and (b) a hygiene finding. The sys_activity and sys_audit_log mirrors of a sys_identity_email row hold a copy of its body. Only the platform administrator can read those copies, so they are no escalation, but they are a credential copied at rest.

    Released line (read from the code, not measured): at @objectstack/plugin-email@17.3.0, sys_email declares no requiredPermissions and its body fields are not internal. Under ruling A of #21993, a fix reaches 17.x through the release/17.x branch that cloud pins. That branch does not exist yet; it is cut at the v18 opening, and this fix is owed there as a backport.

    Next: the maintainer ordered the fix dispatched (「24625 这种你直接派发处理」). The claim follows.

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: epic seat round · 2026-10-19T22:40Z
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve (the card's assignee)
    Branch: claude/issue-24625-identity-mail-read-gate
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:services
    Seat: epic:#15194 (the epic lane)
    The maintainer's order, verbatim: 「24625 这种你直接派发处理」. This claim takes the card from measurement to fix: the PR Closes #24625.
    File surface, read on origin/main at or after bfc1539fe. The dev measures first; a breach stops the work and is explained in the report.

    • The read side of identity mail in sys_email: the object declaration (packages/platform-objects/src/audit/sys-email.object.ts), any read middleware or row/field gate plugin-email owns, and the search projection of sys_email.
    • The mirrors: the sys_activity and sys_audit_log writers (plugin-audit and the activity writer). Identity-mail bodies, from both sys_email and sys_identity_email, are never copied in clear; mask them as credential fields are masked.
    • Tests and fixtures for the principal × door matrix in the measured record above.
    • ⛔ Not the ceremony files of feat(cli): 18.x step 5: the upgrade ceremony steps behind the refusal (webhook re-source, sibling moves, 9c, 8e NOT NULL) #24581 / feat(cli,runtime)!: 18.x step 6: open the upgrade ceremony for v17 and 18.0 databases (and the deferred flips) #24582 (step 5b moves these rows; a parallel session owns it). ⛔ Not docs/adr/**. ⛔ Not content/docs/releases/. ⛔ No credential value in any test name, fixture comment, log line or report.
      Container & model: M, mode:cloud, model: TIER_DEFAULT
      Clause-②: yes (narrowing). A principal loses read access to identity-mail bodies. The dev re-measures.
      Responsibility: n/a. A measured security finding (p1) and the maintainer's order.
      Serial constraints cleared:
    • Steps 5+6 (claude/issue-24581-ceremony-open) touch the ceremony, not the read side. If both touch plugin-email, whichever lands second merges main.
      Merge gate: the PR opens as a draft. The seat runs a CONTRACT_REVIEW_TIER review, and it lands with R1–R4 holding and the harness green.
  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24625,
    "status": "done",
    "branch": "claude/issue-24625-identity-mail-read-gate",
    "pr": "#24688",
    "session": "session_01CAtkMwk5tY1jPaUCdKosnk",
    "premise_still_valid": true,
    "summary": "Re-measured on main bfc1539 over the 18.0 fixture (one reset row, one organization-invitation row in sys_email): an organization admin without manage_platform_settings read the reset body on /data list, get and query, in global search, and in both mirrors; the writers stored identity and tenant mail bodies in clear (9 red / 11 green). No reliable identity-row selector exists: magic link and notification mail both carry no related_object and sys_email has no template column, so the fix is field-level. (1) body_text and body_html on sys_email and sys_identity_email declare requiredPermissions manage_platform_settings: the org admin keeps every row without the bodies; body filters are refused; search does not match; the mirror read redaction and the existing value-column guard close the mirrors. (2) plugin-audit's ledger view masks both logs' bodies like a credential field on every snapshot side. (3) A shipped defect the narrowing would trigger: sys_approval_request free-text search auto-included payload_json, which the snapshot guard refuses to any reader withheld any field, so the org admin's whole global search went 403; the object now declares searchableFields = auto-default minus payload_json. Changeset: minor x3, Clause-② yes (narrowing), before / now / remedy.",
    "tests": "Head e7f2dd0. New dogfood identity-mail-read-gate (22 cases, principal x door, platform-admin controls, org-admin non-identity read): 22 passed; on unchanged main 9 red. R2 batch (upgrade-18x-harness, the new matrix, search-skip-unreadable, audit-log-admin-search, activity-text-predicate, signup-organization-census): 6 files, 62 passed. R3 = the harness's v17 case, green. R4 cli integration serve-walled-posture-refusal: 5 passed. R1 pnpm dev --fresh -p 38625: admin signs in, /data/sys_email, /data/sys_identity_email, /data/showcase_task, /search all 200, reset request 200, no error lines, stopped, port free. vitest: platform-objects 1196 passed, plugin-audit 935, plugin-approvals 1140, plugin-email 571; typecheck exit 0 for those four plus dogfood. Ablations via ablation-replace + rebuild + ablation-dist-preflight, each restored to the HEAD blob with git diff HEAD empty: A1 remove sys_email body requiredPermissions: 8 dogfood red, marker present in 4 built files, absent after restore. A2 disable the ledger mask: 4 plugin-audit unit red + 2 dogfood writer red; this leg's preflight exit was read through a pipe and is void, dist reach shown by the dogfood red; absent after restore exit 0. A3 put payload_json back in the approvals search set: 1 unit red + org-admin global search red, marker present then absent.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 74 commands on e7f2dd0; all 74 ran with exit codes written before any pipe; 72 exit 0 on the first pass, 2 exit 3 on prerequisites (check-plugin-teardown-shape --self-test needed its pinned fixture commit fetched; check:dual-build-cjs-loads needed a full pnpm build), both re-run exit 0. --ran reconciliation: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN. check-governed-merges --branch HEAD: NOT governed, 531 changed lines. CI on #24688: not yet read at report time (in_progress).",
    "mcp_calls": "6 — issue_read x4 (#24625 get and comments, #24531 get and comments), plus ToolSearch loads; no MCP write tool used",
    "api_writes": "2 relay strokes: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24688/assignees, read back identical, assignee os-steve; (2) this report, POST /repos//issues/24625/comments. git push not counted.",
    "open_questions": [
    {
    "question": "Field-level gate hides the organization's own mail bodies (notifications, invitations) from the organization admin, not only identity bodies. Accept, or pursue a precise identity-row selector later?",
    "options": ["A accept field-level (taken): no column distinguishes 18.0 magic-link rows from notification mail", "B after the 18.x ceremony moves identity rows out of sys_email (#24581), relax the sys_email body gate back to the tenant wall"],
    "recommendation": "A now, B as a ceremony follow-up: once no identity row can remain in sys_email, the sys_email body gate has no reason left."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: exception security · evidence: plugin-email@17.3.0 sys_email has the same exposure; backport to release/17.x owed when that branch is cut (seat record 6278938192) · dedupe words: sys_email body backport, 17.x identity mail read gate",
    "carrier: #24581 (承接者) · mirror rows written before this release still hold identity bodies at rest; served only to readers served the body; rewriting them belongs with the ceremony that moves the rows"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and closed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-20T00:36Z. PR #24688 has merged as d1173825b.

    reading value
    contract review on PR #24688 at e7f2dd039, CONTRACT_REVIEW_TIER (narrowing face), PASS, no blocking items
    matrix 22 cases green (9 red on main before the fix), with platform-admin and organization-admin-non-body controls
    R1–R4 as the dev reported in 6279731318

    Delivered.

    • body_text / body_html of sys_email and sys_identity_email are read only with manage_platform_settings. Every door the review checked is closed for every other principal: /data list, get, query and select, body filters, aggregate, global search (including the pinyin companion), both mirrors, export, views and related lists, analytics, and MCP.
    • The audit and activity writers mask both bodies on every snapshot side.
    • sys_approval_request declares its searchableFields, so an organization admin's global search no longer turns into a 403.

    Carried:

    The card closes completed. The dev session is archived.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions