amruthpillai/reactive-resume · error · ORPCError

FORBIDDEN

FORBIDDEN

Error message

FORBIDDEN

What it means

Authorization guard in the storage delete route. After normalizing the requested key and prefixing it with the caller's uploads directory, the handler rejects the request if the key contains path-traversal segments (. or ..) or does not start with uploads/{userId}/. oRPC raises FORBIDDEN (HTTP 403). It is a defense-in-depth ownership check, not a 'file missing' signal.

Source

Thrown at packages/api/src/features/storage/router.ts:98

		.errors({
			NOT_FOUND: {
				message: "The specified file was not found in storage.",
				status: 404,
			},
			FORBIDDEN: {
				message: "You do not have permission to delete this file.",
				status: 403,
			},
		})
		.handler(async ({ context, input }): Promise<void> => {
			const requestedKey = normalizeKey(input.filename);
			const key = requestedKey.startsWith("uploads/")
				? requestedKey
				: normalizeKey(`uploads/${context.user.id}/pictures/${requestedKey}`);
			const userPrefix = `uploads/${context.user.id}/`;

			if (isUnsafeStorageKey(key) || !key.startsWith(userPrefix)) {
				throw new ORPCError("FORBIDDEN");
			}

			const deleted = await storageService.delete(key);

			if (!deleted) throw new ORPCError("NOT_FOUND");
		}),
};

View on GitHub (pinned to 3a5b12e2a4)

Solutions

  1. Only delete files whose path was returned by your own uploadFile call (it is already scoped to uploads/{yourUserId}/pictures/...).
  2. If you must pass uploads/-prefixed keys, ensure the second segment is your own user id.
  3. Never let user input build the filename unsanitized; strip leading slashes and reject any '/'-traversal on the client.
  4. Confirm context.user.id is populated (authenticated session) — an unauthenticated call would never match the prefix.

Example fix

// before: passing a path from another user
await orpc.storage.deleteFile.mutate({ filename: 'uploads/other-user/pictures/x.jpg' }); // 403
// after: delete only your own previously-uploaded path
await orpc.storage.deleteFile.mutate({ filename: myUploadedFile.path });
Defensive patterns

Strategy: validation

Validate before calling

function canDelete(filename: string, userId: string): boolean {
  const norm = filename.trim().replace(/^\/+/, '').split('/').filter(Boolean).join('/');
  if (norm.split('/').some(s => s === '.' || s === '..')) return false;
  const own = `uploads/${userId}/`;
  return norm.startsWith(own);
}

Type guard

function isOwnUploadKey(filename: string, userId: string): filename is string {
  return canDelete(filename, userId);
}

Try / catch

try { await orpc.storage.deleteFile.mutate({ filename }); }
catch (e) { if (isORPCError(e, 'FORBIDDEN')) throw new UserError('You can only delete your own uploads.'); throw e; }

Prevention

When it happens

Trigger: Calling storage.deleteFile with a filename that resolves under another user's uploads/ prefix; passing a fully-qualified uploads/<otherUserId>/... key; including literal '.' or '..' segments in the filename; or supplying a key like 'uploads/' with no user-scoped suffix.

Common situations: Client sends a raw path from a different user's profile picture URL; an MCP/automation client constructs the key manually instead of using the path returned by uploadFile; attempts to delete shared/system files. Legitimately hit only when the caller oversteps its own uploads namespace.

Understand the failure class

Related errors


AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12). Data as JSON: /api/errors/c20060fa2b53375f. Report an issue: GitHub.