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. added
    priority:p1High: required for production / M2
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Oct 19, 2026
  2. 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.
  3. 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.

  4. 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.
  5. 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

  6. 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.

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · maintainer 「24625 不考虑,关卡」 2026-10-20T22:42Z

    The maintainer ruled against a 17.x backport of this fix. Their words, in the epic seat's chat, verbatim: 「24625 不考虑,关卡」. The card was already closed as completed, because the fix shipped on the 18.x line. The seat had kept "a 17.x backport if a release/17.x line is ever cut" as an unfiled follow-up; it is now dropped. The same holds for the 17.x mail-body note that #24934's report named.

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

Metadata

Metadata

Assignees

Labels

domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:epicpriority: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