fix(security): redact git provider secrets from application.one response

findApplicationById eagerly loads the github/gitlab/gitea/bitbucket relations
(needed server-side to clone) including OAuth tokens, the GitHub App private key
and webhook secret. application.one returned them to the client, exposing them to
any member with service:read even when hasGitProviderAccess was false.

Adds redactApplicationGitSecrets() in the application service (blanks the secret
columns, immutably) and applies it to application.one. No client feature reads
these secrets (verified in the frontend); server-side clone paths use
findApplicationById directly, so behaviour is unchanged.

Closes GHSA-hg9j-j5mc-phf5, GHSA-wx75-vxph-2m2f
This commit is contained in:
Mauricio Siu
2026-07-19 21:28:02 -06:00
parent c2c0e9c1c2
commit 68ea9f7771
3 changed files with 119 additions and 1 deletions

View File

@@ -0,0 +1,70 @@
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,6 +13,7 @@ import {
mechanizeDockerContainer,
readConfig,
readRemoteConfig,
redactApplicationGitSecrets,
removeDeployments,
removeDirectoryCode,
removeMonitoringDirectory,
@@ -183,7 +184,7 @@ export const applicationRouter = createTRPCRouter({
}
return {
...application,
...redactApplicationGitSecrets(application),
hasGitProviderAccess,
unauthorizedProvider,
};