August 13, 2026 - A profile deployment could carry another user's address in its avatar identity fields
| Reported on Immunefi | August-12-2026 |
| Mitigation | August-13-2026 |
| Solution Completed | August-13-2026 |
- A profile entity is identified by its pointer, which per ADR-102 is the owner's address, and the profile's content additionally repeats that address inside the metadata as
avatars[].userIdandavatars[].ethAddress. Two descriptions of the same identity therefore travel together: one signed and authorized, one free text written by whoever deployed the entity. - Catalyst authorized only the first of them. The profile access validator compared the entity pointer against the auth-chain owner and rejected a deployment signed by anyone other than the pointer's address, which is the check that keeps an attacker from writing to a victim's pointer. It never read the avatar metadata. Stateless validation was no stronger:
@dcl/schemasrequiresethAddressto look like an address and does not listuserIdin the profile schema'srequiredset, so both fields were only ever checked for shape. - An ordinary wallet could consequently deploy a profile to its own pointer — fully authorized, correctly signed — whose embedded
userIdandethAddressnamed an unrelated victim address. - Lambdas then made the disagreement observable. It selected and filtered the profile by
entity.pointers[0], used that pointer for every ownership lookup, and built the response by spreading the deployed avatar while overriding onlylinks,hasClaimedName, and the nestedavatarobject. Both identity fields were passed through untouched, and the response carries no separate pointer field, so nothing in it stated the address the profile had actually been served for. - Consumers treat those fields as the address of the profile, which is what turned metadata into identity.
decentraland-dappskeys its profile store byprofile.avatars[0].userIdon the batch-load path (modules/profile/reducer.ts), and that store is read by address. Loading the attacker's profile therefore wrote attacker-controlled name and avatar content under the victim's key, and a later lookup for the victim was answered from that entry without a request for the victim's real profile ever being issued. The same pattern existed in otheruserId-keyed consumers reading the profile endpoints. - The effect was impersonation in a viewer's client state: the attacker's name, avatar, and wearables presented as the victim wherever that store is read. It required the attacker's profile to be loaded before the victim's key in the same session, and it did not survive a reload that resolved the victim first.
- Nothing authoritative was altered. The victim's Catalyst entity, pointer, deployment history, names, and on-chain assets were untouched, no signature was forged, and no wallet, funds, or content of the victim's became reachable. ADR-204 has clients obtain profiles from a trusted Catalyst node precisely to prevent avatar impersonation; here the node was honest and the data it returned was self-inconsistent.
- The condition was not limited to the site.
asset-bundle-registry, which serves/profilesto unity-explorer and to social-service-ea, derives its profiles from the content server and from Lambdas, so it relayed the same deployed metadata to those consumers.
Profile identity was expressed twice — once as the signed, authorized pointer and once as free-text metadata — and only the pointer was authorized. No layer required the two to agree, and the read path published the unauthorized copy while omitting the authoritative one, so every consumer that needed an address had nothing but the deployer's word to key on.
Websites & Applications - Medium.
This matched "defacing the sites, totally or partially, resulting in unauthorized visual changes of the site which could impact multiple users". Exploitation needed only an ordinary wallet and the normal deployment path, and the victim did nothing but browse. The realized impact was confined to attacker-controlled name and avatar content being attributed to a victim in a viewer's client state, ordered within a single session and reversible on reload; it did not execute code, alter on-chain ownership, or change the victim's stored profile.
For an address profile, identity is now settled from the pointer at the boundaries it crosses: a deployment whose avatar identity disagrees with the pointer is rejected at admission, and a profile is served with the identity of the pointer it was requested for. The derived store that repeats this data settles and keys it by pointer too, as follow-up hardening. Default profiles are exempt throughout, since their pointer is a name rather than an address.
The report was received on August 12, 2026 at 3:04:58 PM. The solution shipped the next day: Catalyst 8.0.6 and Lambdas 4.13.2 were released together on August 13, closing admission and the read path in the same deployment.
The response shape was deliberately left alone rather than extended with an authoritative pointer field. Because the served userId and ethAddress are now the pointer itself, keying by userId and keying by the requested address became the same thing, which makes the consumers that key from the served identity correct without a client release and keeps the @dcl/schemas profile contract unchanged. It does not reach a consumer that obtains a profile from somewhere other than these endpoints.
@dcl/content-validatorrequires an avatar'suserIdandethAddressto match the entity pointer, reporting which field disagrees. The check lives in the shared profile pointer access path, so both the on-chain and the subgraph access validators inherit it. Only fields that are present are compared, so omittinguserIdkeeps working, and the comparison is case-insensitive so checksummed values pass. Default profiles are exempt: their pointer is a name such asdefault10rather than an address, and their metadata legitimately carries the Decentraland deployer address. Released as@dcl/content-validator@8.1.0.- The rule is gated on a new
PROFILE_IDENTITY_TIMESTAMP(2026-08-13T00:00:00Z, overridable through the environment like the existing ADR timestamps). Profiles deployed before the cutoff keep validating, which matters when a node re-validates historical content while bootstrapping, since a large amount of existing profiles predate the rule. Being a fixed point rather than a relative one, every node agrees on how far back it reaches regardless of when the node started. While the cutoff sits in the past, a new deployment cannot be dated before it to avoid the check: Catalyst independently rejects an entity whose timestamp is further behind wall-clock thanREQUEST_TTL_BACKWARDS, 20 minutes by default. Both values are configuration, so that guarantee holds for the deployed settings rather than by construction. - Catalyst picked the rule up so it runs at deploy time, released as Catalyst 8.0.6 on August 13.
- Lambdas derives both identity fields from
entity.pointers[0]on every returned avatar. The authoritative value was already in scope and used for every ownership lookup in the same function; it is now written back into the response instead of the deployed metadata being forwarded. Default profiles keep their deployed value, since their pointer is a name and pinning would returnethAddress: "default5". Released as Lambdas 4.13.2 on August 13.
asset-bundle-registry serves /profiles to unity-explorer and to social-service-ea from data it takes from the content server and from Lambdas. What it ingests or refetches after the two releases above therefore already carries a settled identity; what it stored before them holds the metadata as deployed and is served from its cache and database as it is, since a stored profile is not revisited until its pointer is deployed to again. A follow-up applies the same rule locally, so that what it serves does not depend on an upstream having done it, and repairs those earlier rows.
- Profiles ingested from the content server have their identity settled from the pointer their deployment was announced for, once on the way in rather than on every read, and a fetched profile is discarded if it carries no pointer, points somewhere other than its deployment, or has no avatars.
- The pointer a profile is stored under is no longer read from its own metadata, and a lookup returns only the pointers that were asked for, so what a request resolves is bound to what it requested.
- A batched migration settles the identity of rows written before the change, which the read path would otherwise keep serving as deployed. Only address pointers are touched, rows that already agree are skipped, and avatar order and every other field are preserved.
- A related lookup defect was fixed in the same change: database pointers were compared lowercased against the raw requested strings, so a request using checksummed addresses always missed the local rows.
Regression coverage now includes deployments whose avatar identity names another address, profiles missing userId, checksummed and mixed-case values, default profile pointers, deployments on either side of the timestamp cutoff, responses whose metadata disagrees with the pointer they were requested for, and requests that ask for pointers in a different case than they are stored in.