fix(publish): stop author bindings publishing the account's email address - #318
Merged
Merged
Conversation
…ress
`{currentEntry.author}` rendered
`{"displayName":"owner@example.com","roleSlug":"owner","roleName":"Owner"}`
into live pages. Three causes stacked.
The token interpolator JSON-stringified any non-scalar "so mistakes stay
visible" — an author-facing debug affordance running in the public publish
path. The entry and loop frames overlaid the internal `PublicDataUserReference`
object into the same flat namespace as user cells, under the very key an author
would reach for. And both setup and `createUser` defaulted `display_name` — a
publicly rendered field — to the account's email address, so the legitimate
`authorName` field leaked it too.
`author` and `publishedBy` are now the display name itself, which is what a
binding to them was always meant to produce, and both are declared in the loop
source's field list so they are discoverable rather than accidental. A
non-scalar binding now renders as missing (honouring any author fallback)
instead of dumping whatever internal shape sat behind the key — the frame
cannot know which of its values are safe to print.
Display names are empty until somebody sets one. Setup takes an optional
`displayName` and the first onboarding screen offers it, since the account page
was previously the only place to set it. Migration 024 clears display names
that are exactly the account's email, because otherwise no existing install
benefits. Admin surfaces already fall back to the email for their own
authenticated display and are unchanged.
Test: `{currentEntry.author}` renders the display name; object and array
bindings render empty and still honour a fallback; scalars are untouched; setup
with and without a name; `createUser` no longer falls back to the email.
React Doctor flags `prefer-useReducer` on AdminPreAuthForm: the new field takes
it from 6 useState calls to 7. Those are independent form scalars plus two
status flags — a reducer would not simplify them, and refactoring an auth form
does not belong in a security fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The leak
A template containing
{currentEntry.author}published this into live HTML:Found while reviewing a generated site — the address was in the page and in every screenshot taken of it. Three independent causes stack.
The interpolator serialised internals.
tokenInterpolation.ts:283JSON-stringified any non-scalar value, commented as keeping "mistakes visible". That is an author-facing debug affordance running unconditionally in the public publish path.The frame put an object where an author would reach.
loopPrefetch.ts:83anddataRows.ts:168,315overlaid the internalPublicDataUserReferenceinto the same flat namespace as user cells, under the keyauthor. A safe-field list already existed atdataRows.ts:500-516— deliberately excludingauthor, includingauthorName— butwalkFieldPathnever consulted it, so the list was advertising forsite_list_loop_sourcesand nothing more.Display names were emails.
setup.ts:73andusers.ts:288both defaulteddisplay_nameto the account's email. That field is rendered publicly through author bindings, so the legitimate{currentEntry.authorName}leaked the address too.publicDataUser.tsdocumented itself as stripping email addresses; it strips theemailfield whiledisplayNamecarries the value.The fix
authorandpublishedByare now the display name itself — what a binding to them was always meant to produce — and both are declared in the loop source's field list so they are discoverable instead of accidental.A non-scalar binding renders as missing, honouring any author-supplied fallback, rather than dumping whatever internal shape sat behind the key. The frame cannot know which of its values are safe to print, so it prints none of them.
Display names are empty until somebody sets one. Setup accepts an optional
displayNameand the first onboarding screen now offers it — previously the account page was the only place to set it, after the fact. Migration024clears display names that are exactly the account's email, since otherwise no existing install benefits.Admin surfaces are unchanged:
UserAvatar.tsx:39,AccountMenuButton.tsx:57,ContentSettingsPanel.tsx:91,ActivityWidget.tsx:190andContentPage.tsx:144already fall back to the email for their own display. That fallback is authenticated-only and stays.Verification
Unit —
authorBindingLeak.test.ts:{currentEntry.author}renders the display name; object and array bindings render empty and still honour a fallback; scalars untouched.setupDisplayName.test.ts: setup with, without, and with a whitespace-only name;createUserno longer falls back to the email.Live — a fresh install now stores
display_name = ''rather than the address. An existing instance carryingowner@masterclass.invalidin that field had it cleared by migration 024 on next boot, verified before and after against two real databases.Suite: 6470 pass / 0 fail. Lint and
tsc --noEmitclean.React Doctor flags
prefer-useReduceronAdminPreAuthForm— the new field takes it from 6useStatecalls to 7. Those are independent form scalars plus two status flags; a reducer would not simplify them, and refactoring an auth form does not belong in a security fix.Follow-ups (not in this PR)
Two further defects found in the same investigation, planned as separate PRs: entry routes serve the oldest published page's runtime manifest rather than their own (
getLatestPublishedSiteSnapshotisorder by created_at asc limit 1, and itsruntime_assets_jsonrides along by accident); andcontent_set_document_statussilently reassigns the editor's active document, which also discards a human's unsaved draft.🤖 Generated with Claude Code