Files
dokploy/packages/server/src/services/git-provider.ts
T
Mauricio Siu 071d9eacee fix(security): enforce org-scope on git provider read endpoints
The .one / getRepositories / getBranches / testConnection handlers for
github, gitlab, gitea and bitbucket resolved a provider by bare id under
protectedProcedure and returned the full row (OAuth tokens, app private
keys, webhook secrets) with no organization or entitlement check, letting
any authenticated user read another org's git-provider credentials (IDOR).

Adds assertGitProviderAccess() in the git-provider service (rejects cross-org
with NOT_FOUND and non-entitled same-org with FORBIDDEN, reusing the existing
getAccessibleGitProviderIds entitlement logic) and calls it in every read
handler before returning secret-bearing data.

Closes GHSA-66r5-5m5f-57vg
2026-07-19 20:55:53 -06:00

149 lines
4.3 KiB
TypeScript

import { db } from "@dokploy/server/db";
import { gitProvider, member } from "@dokploy/server/db/schema";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
export type GitProvider = typeof gitProvider.$inferSelect;
export const removeGitProvider = async (gitProviderId: string) => {
const result = await db
.delete(gitProvider)
.where(eq(gitProvider.gitProviderId, gitProviderId))
.returning();
return result[0];
};
export const findGitProviderById = async (gitProviderId: string) => {
const result = await db.query.gitProvider.findFirst({
where: eq(gitProvider.gitProviderId, gitProviderId),
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Git Provider not found",
});
}
return result;
};
export const updateGitProvider = async (
gitProviderId: string,
input: Partial<GitProvider>,
) => {
return await db
.update(gitProvider)
.set({
...input,
})
.where(eq(gitProvider.gitProviderId, gitProviderId))
.returning()
.then((response) => response[0]);
};
// Returns true if the user can edit the git source configuration of an existing
// deploy that is connected to the given provider.
// Owner/admin: always yes.
// Member: only if they own the provider or it's shared with the org.
// Being in accessedGitProviders only grants permission to connect NEW deploys,
// not to modify the git config of an existing deploy owned by someone else.
export const canEditDeployGitSource = async (
gitProviderId: string,
session: { userId: string; activeOrganizationId: string },
): Promise<boolean> => {
const { userId, activeOrganizationId } = session;
const memberRecord = await db.query.member.findFirst({
where: and(
eq(member.userId, userId),
eq(member.organizationId, activeOrganizationId),
),
columns: { role: true },
});
if (memberRecord?.role === "owner") return true;
const provider = await db.query.gitProvider.findFirst({
where: eq(gitProvider.gitProviderId, gitProviderId),
columns: { userId: true, sharedWithOrganization: true },
});
if (!provider) return false;
return provider.userId === userId || provider.sharedWithOrganization;
};
export const getAccessibleGitProviderIds = async (session: {
userId: string;
activeOrganizationId: string;
}): Promise<Set<string>> => {
const { userId, activeOrganizationId } = session;
const allOrgProviders = await db.query.gitProvider.findMany({
where: eq(gitProvider.organizationId, activeOrganizationId),
columns: {
gitProviderId: true,
userId: true,
sharedWithOrganization: true,
},
});
const memberRecord = await db.query.member.findFirst({
where: and(
eq(member.userId, userId),
eq(member.organizationId, activeOrganizationId),
),
columns: { accessedGitProviders: true, role: true },
});
if (memberRecord?.role === "owner" || memberRecord?.role === "admin") {
return new Set(allOrgProviders.map((p) => p.gitProviderId));
}
const licensed = await hasValidLicense(activeOrganizationId);
const assignedSet = licensed
? new Set(memberRecord?.accessedGitProviders ?? [])
: new Set<string>();
const result = new Set<string>();
for (const p of allOrgProviders) {
if (
p.userId === userId ||
p.sharedWithOrganization ||
assignedSet.has(p.gitProviderId)
) {
result.add(p.gitProviderId);
}
}
return result;
};
/**
* 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).
*/
export const assertGitProviderAccess = async (
session: { userId: string; activeOrganizationId: string },
provider: { gitProviderId: string; organizationId: string },
) => {
if (provider.organizationId !== session.activeOrganizationId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Git provider not found",
});
}
const accessibleIds = await getAccessibleGitProviderIds(session);
if (!accessibleIds.has(provider.gitProviderId)) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have access to this git provider",
});
}
};