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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions