HeyPuter/puter · error · HttpError

forbidden

forbidden

Error message

Not allowed to update this item

What it means

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.

Source

Thrown at src/backend/controllers/desktop/DesktopController.js:244

    async #updateFSEntry(actor, { item_uid, item_path }, patch) {
        if (!item_uid && !item_path) {
            throw new HttpError(400, 'Missing `item_uid` or `item_path`', {
                legacyCode: 'bad_request',
            });
        }

        const entry = item_uid
            ? await this.stores.fsEntry.getEntryByUuid(item_uid)
            : await this.stores.fsEntry.getEntryByPath(item_path);
        if (!entry) {
            throw new HttpError(404, 'Item not found', {
                legacyCode: 'not_found',
            });
        }

        const actorUserId = actor?.user?.id;
        if (typeof actorUserId !== 'number' || entry.userId !== actorUserId) {
            throw new HttpError(403, 'Not allowed to update this item', {
                legacyCode: 'forbidden',
            });
        }

        const keys = Object.keys(patch);
        const setClause = keys.map((k) => `\`${k}\` = ?`).join(', ');
        const values = keys.map((k) => patch[k]);

        await this.db.write(
            `UPDATE \`fsentries\` SET ${setClause} WHERE \`id\` = ?`,
            [...values, entry.id],
        );

        await this.stores.fsEntry.invalidateEntryCacheByUuid(entry.uuid);
    }

    onServerStart() {}
    onServerPrepareShutdown() {}

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Only attempt preference writes on folders owned by the authenticated user; for shared folders, preferences belong to the owner, not the viewer.
  2. Ensure the access token is a full user-scoped token (requireUserActor + allowFullAccessToken are both on) for the owner.
  3. If the actor shape changed, confirm auth middleware still populates req.actor.user.id as a number.

Example fix

// no code fix — this is a contract violation; verify ownership first
const entry = await fsEntry.getEntryByPath(p);
if (entry.userId !== currentUser.id) throw new Error('not owner');
Defensive patterns

Strategy: validation

Validate before calling

const entry = await fsEntry.getEntryByPath(p);
if (!entry || entry.userId !== currentUser.id) {
  throw new Error('not owner');
}

Type guard

const isOwner = (entry: { userId: number }, user: { id: number }): boolean =>
  entry.userId === user.id;

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/d2a1ea2e1e631b3a. Report an issue: GitHub.