Skip to content

Generated forms and write routes expose credential fields; @formExclude does not block writes #484

Closed

Description

@dmealing

Generated admin forms and generated write routes can expose credential fields.

Observed in two applications built on earlier releases (0.19.x to 1.1.0; not yet re-checked on 1.1.4):

  • Generated forms rendered secret hashes (for example a session tokenHash and a credential pinHash) as editable inputs.
  • Marking a field @formExclude kept it out of the generated form, but the generated create/update routes still accepted writes to that field. Each application had to add allowlists so new entities emitted no routes, plus a hand-written server guard.

Expected:

  • A field that is excluded from forms, or that is marked as a credential/secret, is also refused by the generated write routes (create, update, patch) and is never returned by generated read routes unless explicitly allowed.
  • The same rule in every port that generates forms or routes, pinned by the API-contract conformance corpus.

First step: reproduce on 1.1.4 in each port and record which ports are affected.

Activity

  1. added this to the 1.2 milestone on Oct 28, 2026
  2. dmealing commented on Oct 28, 2026

    @dmealing
    MemberAuthor

    Fix is up in #503.

    Which ports were affected: all five. On main the new fixtures/api-contract-conformance/secret-fields/ corpus (generated lane) failed in TypeScript, Java, Kotlin, Python and C#, both scenarios in each: a GET returned pinHash, and a PATCH wrote the forged value into tokenHash and pinHash. After the fix both scenarios pass in all five ports, and the write path is also asserted by reading the stored row back.

    Existing vocabulary only, no new attributes. A credential is a field that declares or inherits a child view.password. A field is write-denied when its effective @formExclude is true or it is a credential. Generated create, PATCH and PUT ignore write-denied fields (stripped, never a 400, like @mutability: writeOnce). A credential key is omitted from list, get, write echoes, M:N targets and TPH rows. A @formExclude-only field is still returned. Filter and sort allowlists are unchanged.

    Judgment calls to review:

    • view.password is the credential marker because it is the only credential vocabulary that exists. There is no override to let a generated route write a denied field; hand-write the route for that.
    • Behaviour change for upgraders: a @required field that is also write-denied makes the generated create fail, because the value is dropped. This is in the CHANGELOG entry and docs/features/api-contract.md.
    • Served reports are redacted in every port: a report that groups by a credential, or takes its min or max, omits the column from the list route and its export, and the column is neither filterable nor sortable.
    • Some TPH shared-column cases, where one subtype marks a column and another does not, are pinned by codegen text checks, not an HTTP scenario.

    The same PR also carries the five docs recipes and a clear error for dialect: "d1" (use "sqlite"). The Workers/D1, drizzle-kit and auth-scope recipes are typechecked, not run.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:codegenCode generationbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions