mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 13:21:11 +00:00
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.
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user