{"record":{"id":"d2a1ea2e1e631b3a","repo":"HeyPuter/puter","slug":"forbidden-d2a1ea","errorCode":"forbidden","errorMessage":"Not allowed to update this item","messagePattern":"Not allowed to update this item","errorType":"http","errorClass":"HttpError","httpStatus":403,"severity":"error","filePath":"src/backend/controllers/desktop/DesktopController.js","lineNumber":244,"sourceCode":"    async #updateFSEntry(actor, { item_uid, item_path }, patch) {\n        if (!item_uid && !item_path) {\n            throw new HttpError(400, 'Missing `item_uid` or `item_path`', {\n                legacyCode: 'bad_request',\n            });\n        }\n\n        const entry = item_uid\n            ? await this.stores.fsEntry.getEntryByUuid(item_uid)\n            : await this.stores.fsEntry.getEntryByPath(item_path);\n        if (!entry) {\n            throw new HttpError(404, 'Item not found', {\n                legacyCode: 'not_found',\n            });\n        }\n\n        const actorUserId = actor?.user?.id;\n        if (typeof actorUserId !== 'number' || entry.userId !== actorUserId) {\n            throw new HttpError(403, 'Not allowed to update this item', {\n                legacyCode: 'forbidden',\n            });\n        }\n\n        const keys = Object.keys(patch);\n        const setClause = keys.map((k) => `\\`${k}\\` = ?`).join(', ');\n        const values = keys.map((k) => patch[k]);\n\n        await this.db.write(\n            `UPDATE \\`fsentries\\` SET ${setClause} WHERE \\`id\\` = ?`,\n            [...values, entry.id],\n        );\n\n        await this.stores.fsEntry.invalidateEntryCacheByUuid(entry.uuid);\n    }\n\n    onServerStart() {}\n    onServerPrepareShutdown() {}","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/desktop/DesktopController.js#L226-L262","documentation":"Thrown by #updateFSEntry (403 forbidden) when the resolved fsentry's userId does not equal the actor's user id, or the actor has no valid numeric user id. This is the ownership gate: you can only set layout/sort on folders you own, even if the path resolves.","triggerScenarios":"POST /set_layout or /set_sort_by referencing another user's folder by guessed uid/path; a shared/hosted item where the caller is not the owner; a token whose actor.user.id is missing or non-numeric (mis-issued or anonymous actor).","commonSituations":"Using a public-share path to try to write preferences on someone else's folder; an internal/service token missing the user scope; race where the folder was transferred to another user between read and write.","solutions":["Only attempt preference writes on folders owned by the authenticated user; for shared folders, preferences belong to the owner, not the viewer.","Ensure the access token is a full user-scoped token (requireUserActor + allowFullAccessToken are both on) for the owner.","If the actor shape changed, confirm auth middleware still populates req.actor.user.id as a number."],"exampleFix":"// no code fix — this is a contract violation; verify ownership first\nconst entry = await fsEntry.getEntryByPath(p);\nif (entry.userId !== currentUser.id) throw new Error('not owner');","handlingStrategy":"validation","validationCode":"const entry = await fsEntry.getEntryByPath(p);\nif (!entry || entry.userId !== currentUser.id) {\n  throw new Error('not owner');\n}","typeGuard":"const isOwner = (entry: { userId: number }, user: { id: number }): boolean =>\n  entry.userId === user.id;","tryCatchPattern":null,"preventionTips":["Only write preferences on folders you own.","Use a full user access token.","Shared folders' preferences belong to the owner, not the viewer."],"tags":["authorization","desktop","forbidden","ownership","fsentry"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}