Skip to content

fix(user): never serialise password digests or SSO tokens - #341

Merged
stakach merged 1 commit into
masterfrom
fix/user-ignore-serialize-secrets
Nov 12, 2026
Merged

stakach merged 1 commit into
masterfrom
fix/user-ignore-serialize-secrets

Conversation

@viv-4

@viv-4 viv-4 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Problem

User#to_json included these fields:

  • password_digest
  • access_token / refresh_token (stored SSO tokens)
  • expires_at / expires
  • password, if it was set but not yet saved

#340 made sure the to_public_*, to_admin_* and to_group_* views never
include these fields, but plain to_json still did. So any API route that
returns a raw User exposed them. rest-api has been closing these routes
one at a time: /users in PlaceOS/rest-api#461, and /group_users in a
follow-up.

Fix

Those six attributes now have ignore_serialize: true. The attribute
macro passes the option through to JSON::Field and YAML::Field, so it
only affects output:

Path Effect
to_json (and any future to_yaml) secrets omitted
from_json / from_trusted_json unchanged: secrets still read
Changefeeds (rebuild models from Postgres row_to_json) unchanged: still receive secrets
History diffs/snapshots (attributes_json / changes_json) unchanged: password changes still recorded as [REDACTED]
Persistence (DB::Field) unchanged

Tests

In the existing sensitive field serialisation block of spec/user_spec.cr:

  • New raw view (user.to_json). It goes through the same checks as
    the other views: no secret keys, and no digest or token values. It
    failed before the fix, leaking 5 keys.
  • Unsaved plain-text password never appears in to_json.
  • Secrets are still read from trusted JSON, so changefeeds keep working.

After merge

  • Release models, and bump placeos-models in rest-api (and any other
    service that renders users).
  • Regenerate rest-api's OPENAPI_DOC.yml, since the PlaceOS.Model.User
    schema loses these fields.

A raw `User#to_json` included `password_digest`, `access_token`,
`refresh_token`, `expires_at`, `expires` and any unsaved plain text
`password`. Any API route that returns a raw `User` therefore leaked
them; rest-api has had to fix this route by route (#461, group_users).

Mark those attributes `ignore_serialize: true`. Only output is
affected:

- They are still read from JSON, so changefeeds (which rebuild models
  via `from_trusted_json`) keep receiving them.
- History diffs and snapshots are built from `attributes_json` /
  `changes_json`, which don't use `JSON::Field`, so password changes are
  still recorded as `[REDACTED]`.
- Persistence goes through `DB::Field` and is unchanged.

Specs add the raw `to_json` view to the sensitive field serialisation
checks, plus checks for an unsaved plain text password and that secrets
are still read from trusted JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@viv-4
viv-4 requested a review from stakach November 12, 2026 03:00
@github-actions github-actions Bot added the type: bug something isn't working label Nov 12, 2026

@stakach stakach left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@stakach
stakach merged commit 38cfed6 into master Nov 12, 2026
8 checks passed
@stakach
stakach deleted the fix/user-ignore-serialize-secrets branch November 12, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: bug something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants