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:
Mauricio Siu
2026-09-01 03:22:52 -06:00
parent e1a432d5c8
commit a42614004f
5 changed files with 72 additions and 2 deletions
@@ -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 }) => {
+11
View File
@@ -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;
}),
+11
View File
@@ -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
+11
View File
@@ -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 }) => {
+27 -2
View File
@@ -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";
};