Skip to content

feat(objectql): mask password fields on the generic read path (#2036)#3170

Merged
os-zhuang merged 3 commits into
mainfrom
claude/password-field-plaintext-leak-58c4gf
Jul 18, 2026
Merged

feat(objectql): mask password fields on the generic read path (#2036)#3170
os-zhuang merged 3 commits into
mainfrom
claude/password-field-plaintext-leak-58c4gf

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #2036. A password-typed field declared on a non-auth object (e.g. showcase_field_zoo.f_password) round-tripped plaintext through the generic CRUD engine — it was neither one-way hashed nor masked-on-read the way secret is. Someone modeling a password field on a custom object reasonably expects credential-grade handling; today they silently got plaintext storage and plaintext reads, a runtime/security trap the static gates don't catch.

This implements the issue's leaning decision (option 1 + 3): mask on read like secret, plus a non-fatal author-time warning. The rationale is recorded in ADR-0100, which this PR also unifies to cover both credential channels (secret and password) — giving the previously-dangling "secret field channel" comment references a real home.

What changed

  • Mask on readpassword fields are masked to SECRET_MASK (••••••••) in find / findOne (and therefore $expand, which re-enters find). A new collectMaskedReadFields helper returns every secret field plus every generic password field; maskSecretFields consumes it. secret behavior is unchanged.
  • Plaintext at rest, by design — unlike secret, a password value is not encrypted and gets no sys_secret row; it's stored verbatim and needs no CryptoProvider. One-way hashing was rejected (option 2): it only makes sense for credential verification, which a non-auth object never does, and doing it in the engine would stand up a second, unmanaged credential store.
  • Echoed-mask write guard — because a read now returns the mask, the write path drops any masked field (secret or password) whose incoming value equals SECRET_MASK, so a form round-trip that echoes the mask is a no-op rather than overwriting the stored value.
  • managedBy: 'better-auth' exemption — the auth subsystem reads its identity rows through the engine, so masking a credential column there would break login. Better-auth objects are exempt. Today this is a safety net, not load-bearing: no shipped identity object even declares a password-typed field (sys_account.password is a hashed text column) — pinned by a platform-objects test so retyping it becomes a deliberate decision.
  • Author-time warningObjectSchema.create() emits a deduped console.warn when a password field is declared on a non-better-auth object, steering authors to Field.secret or the auth subsystem. A warning, not a build error — password now has a defined generic-path contract, and the field-zoo example intentionally exercises every field type, so a hard error would be self-inflicted breakage.
  • Docs — corrected the field-type gallery, which wrongly called password "one-way hashed" (flagged by the docs-drift check).

Tests

  • packages/objectql/src/secret-fields.test.ts — new password-masking block: plaintext at rest (no crypto, no sys_secret), masked on find/findOne, unset stays null, echoed-mask no-op, new value replaces, and the better-auth exemption.
  • packages/spec/src/data/object.test.ts — warns once for a generic password field, deduped, silent for better-auth and for secret.
  • packages/platform-objects/src/platform-objects.test.ts — pins that no shipped object declares a password-typed field, and that sys_account.password is text.
  • packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.tsf_password upgraded from present to masked (verified reading back SECRET_MASK over real HTTP).

Verification

All run green from a fresh worktree:

  • @objectstack/objectql — 12 tests (7 new)
  • @objectstack/spec — object.test.ts, 103 tests
  • @objectstack/platform-objects — 120 tests
  • @objectstack/dogfood — field-zoo HTTP round-trip, 46 tests (real REST → engine path)
  • Full dependency graph builds/typechecks (61 turbo tasks) and changed files lint clean.

Follow-up

aggregate() masks neither secret nor password (a pre-existing gap for secret). Post-hoc masking of aggregate output would corrupt group keys; the correct fix is to reject aggregations that reference a credential field. Noted in ADR-0100 and tracked in #3171.

🤖 Generated with Claude Code


Generated by Claude Code

A `password`-typed field on a non-auth object round-tripped plaintext
through the generic CRUD engine — neither hashed nor masked the way
`secret` is. Someone modeling a `password` field on a custom object
reasonably expects credential-grade handling; they silently got plaintext
storage and plaintext reads.

Mask `password` to SECRET_MASK on the generic read path (find/findOne, and
$expand which re-enters find), mirroring `secret`. Unlike `secret`, a
password value stays plaintext at rest — no encryption, no sys_secret row,
no CryptoProvider required. An echoed mask is dropped on write so a form
round-trip does not overwrite the stored value. Objects marked
`managedBy: 'better-auth'` are exempt so the auth subsystem's own reads
(which go through the engine) still see the stored value; a platform-objects
pin asserts no shipped identity object even declares a `password` field
today. `ObjectSchema.create()` now warns (non-fatally, deduped per object)
when a `password` field is declared on a non-auth object, steering authors
to `secret` or the auth subsystem.

Decision and rationale recorded in ADR-0100; the aggregate() masking gap
(pre-existing for `secret` too) is noted there as a follow-up.

- objectql: add collectMaskedReadFields; wire mask/echoed-mask paths
- spec: author-time warning in ObjectSchema.create(); correct field.zod docs
- tests: password-masking units, warning units, platform-objects pin,
  dogfood field-zoo f_password upgraded present -> masked
- examples: field-zoo label 'Password (one-way hash)' -> '(masked on read)'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJrKxe1jXGudFT3nUks1yN
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 18, 2026 6:28am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, @objectstack/platform-objects, packages/qa, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/qa, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/platform-objects, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…not hashed

The field-type gallery said a `password` field is "stored as a one-way
hash" — inaccurate for a generic object, where (per ADR-0100 / this PR) the
value is plaintext at rest but masked to •••••••• on read. One-way hashing
is owned by the auth subsystem and applies only to its identity tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJrKxe1jXGudFT3nUks1yN
… password)

Reframe ADR-0100 as the single record for both credential-bearing field
types: it now documents the pre-existing `secret` channel (encrypted at
rest, masked on read, fail-closed) alongside the new `password` masking
decision (#2036), and gives the long-dangling "secret field channel"
comment references a real home. Renamed the file to reflect the unified
scope. All code references use the stable ADR-0100 number, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJrKxe1jXGudFT3nUks1yN
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 06:24
@os-zhuang
os-zhuang merged commit 5f5762d into main Jul 18, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/password-field-plaintext-leak-58c4gf branch July 18, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

password-typed field on non-auth objects returns plaintext (no hash, no mask)

2 participants