From 91aa06e3a32af4acaff55b65e38755977ac001ec Mon Sep 17 00:00:00 2001 From: merge-check <140916359+agentHits@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:16:58 -0400 Subject: [PATCH] fix: restrict host-level schedule creation to owners and admins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Что: - schedule.create для расписаний без сервиса (типы dokploy-server и server) теперь вызывает assertHostScheduleAccess, как уже делают update, delete, runManually и list. - Тесты: участник не может создать расписание dokploy-server или server (FORBIDDEN, в базу ничего не пишется); админ может создать dokploy-server. Зачем: - Роль member по умолчанию имеет schedule.create. Без этой проверки любой участник мог создать расписание dokploy-server, и его скрипт запускался от root в контейнере Dokploy с доступом к Docker-сокету, /etc/dokploy и переменным панели, то есть ко всем секретам. Для типа server скрипт запускался по SSH на сервере. - После восстановления запуска расписаний dokploy-server (PR #9) это стало реально исполнимым. Риски: - Участники без роли owner/admin больше не могут создавать расписания уровня сервера. Расписания приложений и compose не меняются. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/schedule/schedule-router-boundary.test.ts - на старом коде 2 новых теста падают (участник создаёт расписание), после правки проходят; все тесты __test__/schedule: 32 прошли. Полный прогон: 2096 прошли, 1 пропущен, 8 упали только в *.real.test.ts (нет сокета Docker Desktop). bun run typecheck - 4/4 без ошибок. bunx biome check для изменённых файлов - 0 ошибок. - Ограничения: на dev ещё не выкатывалось. What: - schedule.create for schedules without a service (dokploy-server and server types) now calls assertHostScheduleAccess, as update, delete, runManually and list already do. - Tests: a member cannot create a dokploy-server or server schedule (FORBIDDEN, nothing is written); an admin can create a dokploy-server schedule. Why: - The member role has schedule.create by default. Without this check any member could create a dokploy-server schedule, and its script ran as root in the Dokploy container with access to the Docker socket, /etc/dokploy and the panel's environment, that is, to every secret. For the server type the script ran over SSH on the server. - Once dokploy-server schedule runs were restored (PR #9), this became exploitable. Risks: - Members without the owner/admin role can no longer create server-level schedules. Application and compose schedules are unchanged. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/schedule/schedule-router-boundary.test.ts - on the old code the 2 new tests fail (a member creates the schedule), after the fix they pass; all tests in __test__/schedule: 32 passed. Full run: 2096 passed, 1 skipped, 8 failed only in *.real.test.ts (no Docker Desktop socket). bun run typecheck - 4/4 clean. bunx biome check on the changed files - 0 errors. - Limitations: not deployed to dev yet. Co-Authored-By: Claude Opus 5.5 --- .../schedule/schedule-router-boundary.test.ts | 44 +++++++++++++++++++ apps/dokploy/server/api/routers/schedule.ts | 1 + 2 files changed, 45 insertions(+) diff --git a/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts b/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts index 9a3524dee1e..b6b35a35315 100644 --- a/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts +++ b/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts @@ -162,6 +162,50 @@ describe("schedule service/server binding boundary", () => { ); }); + it.each([ + { scheduleType: "dokploy-server" as const }, + { scheduleType: "server" as const, serverId: "server-1" }, + ])( + "rejects members creating $scheduleType schedules before persistence", + async (hostInput) => { + mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); + + await expect( + createCaller().create({ + ...scheduleInput, + ...hostInput, + script: "id", + }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + + expect(mocks.createSchedule).not.toHaveBeenCalled(); + expect(mocks.scheduleJob).not.toHaveBeenCalled(); + }, + ); + + it("allows admins to create dokploy-server schedules", async () => { + mocks.createSchedule.mockResolvedValueOnce({ + ...scheduleInput, + scheduleId: "host-schedule-1", + scheduleType: "dokploy-server", + }); + + await expect( + createCaller().create({ + ...scheduleInput, + scheduleType: "dokploy-server", + script: "echo ok", + }), + ).resolves.toMatchObject({ scheduleId: "host-schedule-1" }); + + expect(mocks.createSchedule).toHaveBeenCalledWith( + expect.objectContaining({ + scheduleType: "dokploy-server", + organizationId: "org-1", + }), + ); + }); + it("rejects converting an application schedule into a server schedule", async () => { await expect( createCaller().update({ diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts index 93f37f029d7..ede53d8e94b 100644 --- a/apps/dokploy/server/api/routers/schedule.ts +++ b/apps/dokploy/server/api/routers/schedule.ts @@ -162,6 +162,7 @@ export const scheduleRouter = createTRPCRouter({ } } else { await checkPermission(ctx, { schedule: ["create"] }); + await assertHostScheduleAccess(ctx, scheduleType, input.serverId); if (IS_CLOUD && input.scheduleType === "server" && input.serverId) { await assertScheduledJobLimit(