Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.
L2
Unit 07 · Security for member sites
Personal data in logs, emails and the browser
Security for member sitesPage 35 of 36Updated
Well-meaning code causes most member-data incidents: a token in a URL, a whole record in a log line or a script tag, a password in an email. What must never appear, the redaction rules for logs, emails and jobs, and the rule for what a template sends to the browser.
Who this is for. Every developer on a member site, and the reviewer who signs it off. Afterwards you can review a template, a job or an email for personal data, and redact what does not need to be there.
What must never appear
In a log, an email, a URL, a job's payload or a page's source, never:
passwords, in any form, including "temporary" ones sent by email;
tokens and links that sign someone in: session tokens, API keys, OAuth tokens, password reset and sign-in links;
full payment details: card numbers, security codes, bank account numbers;
unmasked contact data where an id would do: email addresses, phone numbers, home addresses.
Logs
When something goes wrong, the platform's log entry records the request that caused it, including the fields a form posted. Your jobs and integrations log what you tell them to. So:
Keep personal data out of URLs. A query string is written in full to logs, browser history, analytics and the Referer header of the next request. Post personal data in a form body; put an id, not an email address, in a link.
Log ids, not people. A user id or a submission id identifies the case for whoever investigates, without copying the person into the log.
Mask what you must include. Where a log line has to show contact data, show enough to recognise it and no more.
And the same event, unredacted, as it should never be written:
ShellNever like this
2026-10-19T09:14:02Z reset requested for sam.example@example.com (Sam Example, 0400 000 000) link=https://www.example.com/token-login?t=…
The second line contains a working sign-in link: anyone who can read the log can become that member until the link expires. Where the logs are lists who can read which log.
Jobs
A job run keeps each action's data and output, and anyone who can open the job's run log can read them. Treat a job's payload as a log:
Send an external system the fields it needs, not the whole record. A CRM that needs a name and an email address does not need the person's addresses and custom fields.
Never pass a password, a reset link or a token through a job.
Keep shared secrets for signing and verifying requests in site data, not in the job's templates, where every run log would show them.
Emails
Never email a password. Send a reset link instead; the platform's forgotten-password flow does.
Treat an email with a sign-in or reset link as a credential, because it is one: the link signs the reader in. Do not copy such emails to a shared inbox, attach them to tickets or forward them.
Put in an email only what the recipient needs. A confirmation can say "your address ending 42 Smith Street" without repeating the whole address.
Send from the client's authenticated domain, so the reset email reaches the inbox rather than the spam folder (The reset email).
What reaches the browser
The rule for templates and client-side state: send only what the page must render. Everything in the page source is readable by the visitor, by every script on the page, analytics and chat widgets included, and by anyone the page is copied to.
The common over-serialisation is a whole record dropped into a script for a front-end component that needed one field. On a member page, the user record printed whole carries the username, the user type and status, the ids of the member's groups and API keys and login tokens, and their custom field values; the component wanted a first name. Build the object you need instead:
Template codeOn a member page: one field, not the record
Two more ways data reaches the browser without being printed:
The JSON representation. Any page requested with Accept: application/json answers with the variables its controller assigned. A member's own pages answer with their own records, which is acceptable. A public page must not be handed anything private in the first place (The same URL as JSON).
The page cache. A public page that printed something personal for one visitor would be wrong for the next; anything personal belongs on a page that is never stored, or in a fragment (Member pages and the page cache).
Checks
Search the templates for json_encode applied to a record, and replace each with an object built from the fields the page needs.
Read the source of a member page, signed in as a test member, for anything the page does not display.
Read the run log of every job that handles people, and the bodies its HTTP requests send.
Trigger each email the site sends and read it as the recipient would.
Search links and redirects for email addresses or tokens in query strings.