Skip to content

Fix: Ensure Event/Volunteer Responses only Include Intended User Fields#54

Merged
ardelato merged 7 commits into
hermesfrom
fix/user-data-serialization
Jun 8, 2026
Merged

Fix: Ensure Event/Volunteer Responses only Include Intended User Fields#54
ardelato merged 7 commits into
hermesfrom
fix/user-data-serialization

Conversation

@ardelato

@ardelato ardelato commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Event pages and the volunteers API previously serialized full User models into their responses. This restricts them to an explicit, minimal set of fields (id, name, and—where appropriate—email) and hardens model serialization so only intended fields are ever included.

Changes

  • User model: expand the serialization $hidden set so internal/account fields are never included by default (defence-in-depth for any path that serializes a user).
  • expandVolunteers(): return an explicit {id, name, email} allowlist. A prior allowlist wasn't taking effect because a loaded Eloquent relation overrode the array during toArray(); the relation is now cleared first so the allowlist (and the existing email-visibility rule) applies.
  • Remove now-redundant ad-hoc makeHidden() calls in the API controllers (covered by the model $hidden set).

ardelato added 5 commits June 5, 2026 09:28
The User model only hid password and remember_token during
serialization, leaving api_token, calendar_hash, recovery tokens,
coordinates, and login activity exposed wherever the model was
json_encoded — including public event pages accessible without
authentication.
… makeHidden

expandVolunteers() previously assigned the full User Eloquent model
to volunteer data that gets json_encoded into public HTML. Replacing
it with an explicit {id, name, email} allowlist prevents future
fields or makeVisible() calls from re-exposing data. The ad-hoc
makeHidden calls in ApiController and UserController for fields now
covered by User::$hidden are also removed.
Asserts that public event pages and the volunteers API endpoint do
not expose api_token, calendar_hash, recovery tokens, or coordinates
in their responses.
…pplies

The expandVolunteers() allowlist added in 58a2e8e was a no-op: $volunteer
is an EventsUsers model with the `volunteer` relation already loaded, so
during toArray() Eloquent's relationsToArray() is merged after the
attributes and overwrites $volunteer['volunteer'] with the full related
User model. As a result public event pages still serialized the entire
user record to anonymous visitors — username, external_user_id, location,
age, gender, country_code, consent timestamps, biography, etc. — and the
$showEmails gating was bypassed, leaking volunteer emails too. Only the
fields in User::$hidden (api_token, calendar_hash, recovery, coordinates,
login activity) were actually protected.

Calling unsetRelation('volunteer') before assigning the allowlist array
removes the loaded relation so the {id, name, email} array survives
serialization.

Also harden PublicEventDataLeakTest:
- approve the group, otherwise the anonymous request 404s before the
  page renders and the public-page assertions never run (vacuous pass)
- extract every embedded "volunteer" object and assert it is limited to
  the {id, name, email} allowlist
- assert volunteer email is not exposed to anonymous viewers
- add username/external_user_id sentinels

Verified: test fails on the pre-fix code and passes with the fix.
…render

The {id, name, email} allowlist introduced for expandVolunteers dropped
user_skills from the serialized volunteer object, but the event page reads
attendee.volunteer.user_skills (EventAttendee.vue) to show each volunteer's
skill count and list. Without it every attendee rendered as "0 skills".

Re-add user_skills to the allowlist (skill associations are not sensitive)
and extend the regression test to assert the volunteer object is exactly
{id, name, email, user_skills}.
@ardelato

ardelato commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

QA 👍 checked against the staging instance.

@sctice-ifixit sctice-ifixit 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.

Claude Code findings.

unsetRelation('volunteer') is what makes the allowlist take effect, and EventsUsers has only the one user-bearing relation, so that closes the leak. The allowlist preserves everything EventAttendee.vue reads (volunteer.name, volunteer.user_skills, plus top-level fullName/profilePath).

Three inline notes: a Zapier heads-up (no action), a test-coverage gap, and a redundant-payload nit.

Comment thread app/Models/User.php
'recovery_expires',
'latitude',
'longitude',
'last_login_at',

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.

Heads-up: latitude, longitude, and last_login_at weren't hidden in the Zapier changes path (mapUserAndAuditToUserChange hid only number_of_logins and the token fields), so they were present in that feed. They're now hidden globally. No evidence anything consumes them; flagging in case a Zap maps those fields.

Comment thread tests/Feature/Events/PublicEventDataLeakTest.php
Comment thread app/Models/Party.php Outdated
ardelato added 2 commits June 8, 2026 08:31
The field-name checks ran against the raw response, but on the public
page path Blade escapes the JSON quotes ("), so the key checks
were no-ops there. Decode HTML entities before the key checks.

latitude/longitude are legitimate public fields on the event itself,
so the bare key can't distinguish event location from a leaked user
location. Guard the volunteer's coordinates with distinctive numeric
value sentinels instead, and drop them from the key-name allowlist.
…nteers

userSkills was serialized twice: at the top level of each volunteer and
nested under volunteer.user_skills. Only the nested copy is consumed
(EventAttendee.vue reads volunteer.user_skills); the top-level copy was
dead weight in the payload. Use a local accumulator so the skills are
serialized once, under the volunteer allowlist only.

@sctice-ifixit sctice-ifixit 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.

CR 🌷 Thanks @ardelato!

@ardelato

ardelato commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

QA 👍

@ardelato ardelato merged commit 1e113af into hermes Jun 8, 2026
@ardelato ardelato deleted the fix/user-data-serialization branch June 8, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants