From a42614004f7e57b6a59f6610bbeeeb650c05c940 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 1 Sep 2026 03:22:52 -0600 Subject: [PATCH] fix: don't leak git provider secrets to non-owner org members gitlab.one, github.one, gitea.one and bitbucket.one returned the full DB record (OAuth access/refresh tokens, client secrets, private keys, webhook secrets, app passwords) to any org member who merely had access to *use* a shared provider (sharedWithOrganization: true), not just its owner or an org owner/admin. Add canViewGitProviderSecrets() and null out the secret fields in each .one response when the caller isn't the provider owner or an org owner/admin. --- apps/dokploy/server/api/routers/bitbucket.ts | 12 ++++++++ apps/dokploy/server/api/routers/gitea.ts | 11 ++++++++ apps/dokploy/server/api/routers/github.ts | 11 ++++++++ apps/dokploy/server/api/routers/gitlab.ts | 11 ++++++++ packages/server/src/services/git-provider.ts | 29 ++++++++++++++++++-- 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/api/routers/bitbucket.ts b/apps/dokploy/server/api/routers/bitbucket.ts index be1437cbd..ce79df1e0 100644 --- a/apps/dokploy/server/api/routers/bitbucket.ts +++ b/apps/dokploy/server/api/routers/bitbucket.ts @@ -1,5 +1,6 @@ import { assertGitProviderAccess, + canViewGitProviderSecrets, createBitbucket, findBitbucketById, getAccessibleGitProviderIds, @@ -55,6 +56,17 @@ export const bitbucketRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const bitbucket = await findBitbucketById(input.bitbucketId); await assertGitProviderAccess(ctx.session, bitbucket.gitProvider); + + if ( + !(await canViewGitProviderSecrets(ctx.session, bitbucket.gitProvider)) + ) { + return { + ...bitbucket, + appPassword: null, + apiToken: null, + }; + } + return bitbucket; }), bitbucketProviders: protectedProcedure.query(async ({ ctx }) => { diff --git a/apps/dokploy/server/api/routers/gitea.ts b/apps/dokploy/server/api/routers/gitea.ts index 0ec08964d..63cfc4e79 100644 --- a/apps/dokploy/server/api/routers/gitea.ts +++ b/apps/dokploy/server/api/routers/gitea.ts @@ -1,5 +1,6 @@ import { assertGitProviderAccess, + canViewGitProviderSecrets, createGitea, findGiteaById, getAccessibleGitProviderIds, @@ -59,6 +60,16 @@ export const giteaRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const gitea = await findGiteaById(input.giteaId); await assertGitProviderAccess(ctx.session, gitea.gitProvider); + + if (!(await canViewGitProviderSecrets(ctx.session, gitea.gitProvider))) { + return { + ...gitea, + clientSecret: null, + accessToken: null, + refreshToken: null, + }; + } + return gitea; }), diff --git a/apps/dokploy/server/api/routers/github.ts b/apps/dokploy/server/api/routers/github.ts index 8650c5a23..dd765dee8 100644 --- a/apps/dokploy/server/api/routers/github.ts +++ b/apps/dokploy/server/api/routers/github.ts @@ -1,5 +1,6 @@ import { assertGitProviderAccess, + canViewGitProviderSecrets, findGithubById, getAccessibleGitProviderIds, getGithubBranches, @@ -28,6 +29,16 @@ export const githubRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const github = await findGithubById(input.githubId); await assertGitProviderAccess(ctx.session, github.gitProvider); + + if (!(await canViewGitProviderSecrets(ctx.session, github.gitProvider))) { + return { + ...github, + githubClientSecret: null, + githubPrivateKey: null, + githubWebhookSecret: null, + }; + } + return github; }), getGithubRepositories: protectedProcedure diff --git a/apps/dokploy/server/api/routers/gitlab.ts b/apps/dokploy/server/api/routers/gitlab.ts index c93637bd1..133610763 100644 --- a/apps/dokploy/server/api/routers/gitlab.ts +++ b/apps/dokploy/server/api/routers/gitlab.ts @@ -1,5 +1,6 @@ import { assertGitProviderAccess, + canViewGitProviderSecrets, createGitlab, findGitlabById, getAccessibleGitProviderIds, @@ -57,6 +58,16 @@ export const gitlabRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const gitlab = await findGitlabById(input.gitlabId); await assertGitProviderAccess(ctx.session, gitlab.gitProvider); + + if (!(await canViewGitProviderSecrets(ctx.session, gitlab.gitProvider))) { + return { + ...gitlab, + secret: null, + accessToken: null, + refreshToken: null, + }; + } + return gitlab; }), gitlabProviders: protectedProcedure.query(async ({ ctx }) => { diff --git a/packages/server/src/services/git-provider.ts b/packages/server/src/services/git-provider.ts index 5a7c6f438..b9d86d3ce 100644 --- a/packages/server/src/services/git-provider.ts +++ b/packages/server/src/services/git-provider.ts @@ -124,8 +124,12 @@ export const getAccessibleGitProviderIds = async (session: { * Authorizes read access to a specific git provider for the current session. * Throws if the provider belongs to a different organization (cross-org IDOR) * or if the caller is not entitled to it within the active organization. - * Must be called before returning any git-provider record that carries secrets - * (OAuth tokens, app private keys, webhook secrets). + * + * This only proves the caller may *use* the provider (e.g. pick it as a repo + * source when creating a deploy) - it does NOT mean they may see its raw + * credentials. Being able to use a shared provider and being able to read its + * OAuth tokens / client secrets / private keys are different privileges; gate + * the latter with canViewGitProviderSecrets before returning secret fields. */ export const assertGitProviderAccess = async ( session: { userId: string; activeOrganizationId: string }, @@ -146,3 +150,24 @@ export const assertGitProviderAccess = async ( }); } }; + +// Being allowed to use a shared provider (assertGitProviderAccess) must not +// imply being allowed to read its raw OAuth tokens / client secrets / private +// keys. Only the provider's owner or an org owner/admin gets those back. +export const canViewGitProviderSecrets = async ( + session: { userId: string; activeOrganizationId: string }, + provider: { userId: string; organizationId: string }, +): Promise => { + if (provider.organizationId !== session.activeOrganizationId) return false; + if (provider.userId === session.userId) return true; + + const memberRecord = await db.query.member.findFirst({ + where: and( + eq(member.userId, session.userId), + eq(member.organizationId, session.activeOrganizationId), + ), + columns: { role: true }, + }); + + return memberRecord?.role === "owner" || memberRecord?.role === "admin"; +};