Update only the fields users may edit

PATCH /me merges the whole request body into the stored user, so adding "role": "admin" turns a member into an administrator. Change only the fields a user may edit on their own profile.

Requires Node.js 22 or newer. No package installation or server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit profile-update.mjs, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same cases.mjs with the same fixtures; locally Node runs your code directly. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. Reproduce the escalation
Run the starter. The role case turns Alice into an admin, and the organization case makes her the owner of an organization she only belongs to: the merge copies whatever the client sends.

2. List what this action may write
Name the writable fields: displayName, bio and preferences. Reject a body with any other key, or copy only those keys. A blocklist of role and ownerId misses Role, emailVerified and every field added to the record later.

3. Apply the allowlist inside nested objects
preferences is writable, but only its theme and digest. Check its keys and values separately, and copy the stored preferences before changing them, so a partial update keeps the other setting.

4. Validate the values you accept
An allowed name does not make any value valid: check that displayName and bio are strings within their limits and that theme and digest are one of the listed values. Ordinary updates, including an empty body, must still succeed.

SUPPLIED CONTRACT
updateProfile(user, body) receives the signed-in user’s stored record and the parsed JSON body of their PATCH /me request (authentication has already run). It returns {status: "updated", user: newRecord}, which the handler stores, or {status: "rejected"}, which the handler answers with 400 and stores nothing. The record is {id, email, emailVerified, role, displayName, bio, preferences: {theme, digest}, organization: {id, name, ownerId}}; organization stands for a related row that an ORM would update through a nested write. A user may change displayName (a string of 1 to 80 characters), bio (a string of up to 280 characters), preferences.theme ("light", "dark" or "system") and preferences.digest ("off", "daily" or "weekly"); a field left out of the body keeps its stored value, inside preferences too. Every other field is set by the server: an update that names one may either be rejected or have that field ignored, but it must never change a server-owned value or add a field at any level. A value that fails these rules is never stored.

REFERENCE
reference.mjs contains an explained solution: Alice’s record starts as member of Red Ltd (owned by u_bob), theme light and digest weekly. A new display name and bio, a bio alone, an empty bio, {preferences: {digest: "daily"}} (theme stays light), {preferences: {theme: "system"}}, both preferences together, an 80-character name and an empty body are applied. role, the case variant Role, email with emailVerified, id and organization.ownerId are refused (or ignored, which the tests also accept), so role stays member, the organization keeps its owner and no field is added. {preferences: {theme: "dark", role: "admin"}} adds nothing to preferences. An empty or 81-character display name, a display name that is an object or null, a 281-character bio, the theme "neon", the digest "hourly", preferences sent as a string or null, and a body that is null are never stored.

SCOPE
The lab binds one self-service action. Changing role, email or organization ownership belongs to separate actions with their own authorization: an admin endpoint, an email-verification flow, an ownership transfer. Real frameworks add their own binding rules: use the input type or schema your framework provides, configured to refuse or drop unknown fields, and check that your ORM does not accept nested writes you did not intend. Response projections, which decide which fields a user may read, are a separate control.

NEXT TASK
Find an endpoint in your code that passes a request body to an ORM update, a merge or Object.assign. Which fields of that entity, including related rows, could a client set today?

SOURCE
https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html
