refactor(security): exclude git provider secrets at query level in findApplicationById

Simpler and consistent with the existing registry column exclusion in the same
query: drop the secret columns from the nested github/gitlab/gitea/bitbucket
relations via columns:{ ...: false } instead of a post-fetch redaction helper.
Server-side clone paths re-fetch providers by id (find{Github,Gitlab,...}ById),
so deployments are unaffected. Column names are validated at compile time by
drizzle's typed columns config.

Closes GHSA-hg9j-j5mc-phf5, GHSA-wx75-vxph-2m2f
This commit is contained in:
Mauricio Siu
2026-07-19 21:43:51 -06:00
parent c0afc48da8
commit ecbaf6060b
3 changed files with 19 additions and 121 deletions

View File

@@ -1,70 +0,0 @@
import { redactApplicationGitSecrets } from "@dokploy/server/services/application";
import { describe, expect, it } from "vitest";
describe("redactApplicationGitSecrets (application.one secret disclosure guard)", () => {
it("blanks github secrets while keeping non-secret fields", () => {
const app = {
applicationId: "app-1",
github: {
githubId: "gh-1",
githubClientId: "public-client-id",
githubClientSecret: "SECRET",
githubPrivateKey: "-----BEGIN KEY-----",
githubWebhookSecret: "whsec",
githubInstallationId: "12345",
},
};
const out = redactApplicationGitSecrets(app);
expect(out.github.githubClientSecret).toBe("");
expect(out.github.githubPrivateKey).toBe("");
expect(out.github.githubWebhookSecret).toBe("");
// Non-secret identifiers must survive so the UI can still show the link.
expect(out.github.githubClientId).toBe("public-client-id");
expect(out.github.githubInstallationId).toBe("12345");
expect(out.applicationId).toBe("app-1");
});
it("blanks gitlab / gitea / bitbucket tokens and passwords", () => {
const app = {
gitlab: { secret: "s", accessToken: "at", refreshToken: "rt" },
gitea: { clientSecret: "cs", accessToken: "at", refreshToken: "rt" },
bitbucket: { appPassword: "pw", apiToken: "tok" },
};
const out = redactApplicationGitSecrets(app);
expect(out.gitlab).toMatchObject({
secret: "",
accessToken: "",
refreshToken: "",
});
expect(out.gitea).toMatchObject({
clientSecret: "",
accessToken: "",
refreshToken: "",
});
expect(out.bitbucket).toMatchObject({ appPassword: "", apiToken: "" });
});
it("does not mutate the original application", () => {
const app = { github: { githubClientSecret: "SECRET" } };
redactApplicationGitSecrets(app);
expect(app.github.githubClientSecret).toBe("SECRET");
});
it("handles applications without any git provider relation", () => {
const app: {
applicationId: string;
github?: null;
gitlab?: null;
gitea?: null;
bitbucket?: null;
} = { applicationId: "app-2", github: null };
expect(redactApplicationGitSecrets(app)).toMatchObject({
applicationId: "app-2",
github: null,
});
});
});

View File

@@ -13,7 +13,6 @@ import {
mechanizeDockerContainer,
readConfig,
readRemoteConfig,
redactApplicationGitSecrets,
removeDeployments,
removeDirectoryCode,
removeMonitoringDirectory,
@@ -184,7 +183,7 @@ export const applicationRouter = createTRPCRouter({
}
return {
...redactApplicationGitSecrets(application),
...application,
hasGitProviderAccess,
unauthorizedProvider,
};

View File

@@ -102,10 +102,24 @@ export const findApplicationById = async (applicationId: string) => {
redirects: true,
security: true,
ports: true,
gitlab: true,
github: true,
bitbucket: true,
gitea: true,
gitlab: {
columns: { secret: false, accessToken: false, refreshToken: false },
},
github: {
columns: {
githubClientSecret: false,
githubPrivateKey: false,
githubWebhookSecret: false,
},
},
bitbucket: { columns: { appPassword: false, apiToken: false } },
gitea: {
columns: {
clientSecret: false,
accessToken: false,
refreshToken: false,
},
},
server: true,
previewDeployments: true,
registry: { columns: { password: false } },
@@ -122,51 +136,6 @@ export const findApplicationById = async (applicationId: string) => {
return application;
};
// Blanks the nested git-provider secret columns before an application is sent to
// a client (server-side clone paths use findApplicationById directly).
export const redactApplicationGitSecrets = <
T extends {
github?: Record<string, unknown> | null;
gitlab?: Record<string, unknown> | null;
gitea?: Record<string, unknown> | null;
bitbucket?: Record<string, unknown> | null;
},
>(
application: T,
): T => {
const blank = (
provider: Record<string, unknown> | null | undefined,
fields: readonly string[],
) => {
if (!provider) return provider;
const redacted = { ...provider };
for (const field of fields) {
if (field in redacted) redacted[field] = "";
}
return redacted;
};
return {
...application,
github: blank(application.github, [
"githubClientSecret",
"githubPrivateKey",
"githubWebhookSecret",
]),
gitlab: blank(application.gitlab, [
"secret",
"accessToken",
"refreshToken",
]),
gitea: blank(application.gitea, [
"clientSecret",
"accessToken",
"refreshToken",
]),
bitbucket: blank(application.bitbucket, ["appPassword", "apiToken"]),
};
};
export const findApplicationByName = async (appName: string) => {
const application = await db.query.applications.findFirst({
where: eq(applications.appName, appName),