mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-06 12:12:16 +00:00
fix(security): authenticate and authorize the GitHub App setup callback
The /api/providers/github/setup callback performed privileged writes (persisting App client_secret/webhook_secret/private_key, re-pointing installations) with no session check, deriving the write target (organizationId/userId on gh_init, githubId on gh_setup) from the attacker-controlled state parameter. An unauthenticated request, or a member with no gitProviders permission, could plant or re-point a provider in any organization. Require a valid session, gate on the gitProviders permission (same guard the tRPC github router uses), derive the create target from the session, and verify the gh_setup provider belongs to the caller's organization.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { createGithub } from "@dokploy/server";
|
import { createGithub, validateRequest } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { hasPermission } from "@dokploy/server/services/permission";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Octokit } from "octokit";
|
import { Octokit } from "octokit";
|
||||||
@@ -21,18 +22,27 @@ export default async function handler(
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
return res.status(400).json({ error: "Missing code parameter" });
|
return res.status(400).json({ error: "Missing code parameter" });
|
||||||
}
|
}
|
||||||
const [action, ...rest] = state?.split(":");
|
|
||||||
// For gh_init: rest[0] = organizationId, rest[1] = userId
|
// This callback performs privileged writes (persisting App secrets, re-pointing
|
||||||
// For gh_setup: rest[0] = githubProviderId
|
// installations) but sits outside tRPC, so it must authenticate and authorize
|
||||||
|
// on its own. The write target is derived from the session, never from `state`.
|
||||||
|
const { user, session } = await validateRequest(req);
|
||||||
|
if (!user || !session?.activeOrganizationId) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
const ctx = {
|
||||||
|
user: { id: user.id },
|
||||||
|
session: { activeOrganizationId: session.activeOrganizationId },
|
||||||
|
};
|
||||||
|
|
||||||
|
const [action] = state?.split(":") ?? [];
|
||||||
|
// gh_init creates a provider, gh_setup re-points an existing one; both require
|
||||||
|
// the gitProviders permission (the same guard the tRPC github router uses).
|
||||||
|
if (!(await hasPermission(ctx, { gitProviders: ["create"] }))) {
|
||||||
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
}
|
||||||
|
|
||||||
if (action === "gh_init") {
|
if (action === "gh_init") {
|
||||||
const organizationId = rest[0];
|
|
||||||
const userId = rest[1] || (req.query.userId as string);
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: "Missing userId parameter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const octokit = new Octokit({});
|
const octokit = new Octokit({});
|
||||||
const { data } = await octokit.request(
|
const { data } = await octokit.request(
|
||||||
"POST /app-manifests/{code}/conversions",
|
"POST /app-manifests/{code}/conversions",
|
||||||
@@ -51,16 +61,32 @@ export default async function handler(
|
|||||||
githubWebhookSecret: data.webhook_secret,
|
githubWebhookSecret: data.webhook_secret,
|
||||||
githubPrivateKey: data.pem,
|
githubPrivateKey: data.pem,
|
||||||
},
|
},
|
||||||
organizationId as string,
|
session.activeOrganizationId,
|
||||||
userId,
|
user.id,
|
||||||
);
|
);
|
||||||
} else if (action === "gh_setup") {
|
} else if (action === "gh_setup") {
|
||||||
|
const githubId = state?.split(":")[1];
|
||||||
|
if (!githubId) {
|
||||||
|
return res.status(400).json({ error: "Missing github provider id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = await db.query.github.findFirst({
|
||||||
|
where: eq(github.githubId, githubId),
|
||||||
|
with: { gitProvider: true },
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!provider ||
|
||||||
|
provider.gitProvider.organizationId !== session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
return res.status(404).json({ error: "Github provider not found" });
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(github)
|
.update(github)
|
||||||
.set({
|
.set({
|
||||||
githubInstallationId: installation_id,
|
githubInstallationId: installation_id,
|
||||||
})
|
})
|
||||||
.where(eq(github.githubId, rest[0] as string))
|
.where(eq(github.githubId, githubId))
|
||||||
.returning();
|
.returning();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user