Skip to content

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

Closed

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 guard — build/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 intended — password 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