Skip to content

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

Description

@os-zhuang

Summary

A password-typed field declared on a non-auth object (e.g. showcase_field_zoo.f_password) is stored by the generic CRUD path as-is — it is neither one-way hashed nor masked-on-read the way secret is. So writing a password field and reading the record back over the data API returns the plaintext value.

Surfaced while completing the dogfood field-zoo round-trip matrix (#2033). Not a regression — this is the current behavior; filing for a deliberate decision rather than fixing inline.

Why it matters

This is a low-code platform where field types are author-driven (often by an AI). Someone modeling a password field on a custom object reasonably expects credential-grade handling. Today they silently get plaintext storage + plaintext reads, with no warning — a runtime/security trap the static gates don't catch.

Current behavior (verified)

  • secret (type==='secret'): encrypt-on-write into sys_secret, opaque ref on the row, masked to SECRET_MASK (••••••••) on read. See packages/objectql/src/secret-fields.ts + engine.ts mask path.
  • password: the generic engine has no hashing/masking for it. Hashing lives only in the auth subsystem's dedicated CRUD pipeline (packages/plugins/plugin-security/.../default-permission-sets.ts comment: "password hashing … rather than the generic CRUD pipeline"). The record validator treats password like text (record-validator.ts). So on a generic object it round-trips plaintext.
  • Dogfood proof: field-zoo-roundtrip asserts f_password only present (persists) precisely because asserting a plaintext-equality contract would be undesirable to lock in.

Options to decide between

  1. Mask-on-read everywhere — treat password like secret in the generic read path (return SECRET_MASK), so plaintext never leaves the engine outside the auth subsystem. Lowest-surprise.
  2. Hash-on-write in the generic path — but one-way hashing only makes sense for credential verification, which non-auth objects don't do; likely wrong fit.
  3. Author-time guardbuild/lint error (or warn) when a password field is declared on a non-auth object, steering authors to secret or the auth user object.
  4. Document as intendedpassword is auth-subsystem-only by contract; generic use is unsupported. Weakest (silent trap remains).

Leaning 1 (+ maybe 3 as a guardrail). Needs a deliberate call.

Acceptance

  • Decision recorded (ADR if it changes the read contract).
  • A dogfood/unit test pins the chosen behavior for password on a generic object.

Found via #2025 / #2028 / #2033 (field-type round-trip fidelity work).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions