Summary
The gitProvider.getAll tRPC procedure in apps/dokploy/server/api/routers/git-provider.ts returns plaintext credentials (RSA private keys, OAuth client secrets, webhook secrets, access tokens) for every configured git provider (GitHub, GitLab, Bitbucket, Gitea) to any authenticated user with permission to list providers. This includes:
- GitHub:
githubPrivateKey (full RSA PEM-encoded private key), githubClientSecret (40-char hex), githubWebhookSecret (40-char hex)
- GitLab:
secret, accessToken, refreshToken (same shape inferred from schema)
- Bitbucket:
appPassword, apiToken
- Gitea:
clientSecret, accessToken, refreshToken
Verified persists through v0.29.4 (latest release at time of report). The affected router file is unchanged between v0.26.5 and v0.29.4 per gh api repos/Dokploy/dokploy/compare/v0.26.5...v0.29.4.
Impact
Any user / API token with the LIST permission on git providers can extract the GitHub App private key. With the private key + clientId + installationId (also returned), an attacker can mint installation access tokens for any repo the App has access to — a privilege escalation from "can list git providers on this Dokploy instance" to "can push to / read from every repo connected to this Dokploy".
Blast radius is amplified by the fact that the leaked tokens are long-lived (App-level, not session-scoped). Rotation requires manual GitHub-side regeneration.
Reproducer
// Authenticated as any user with git-provider list permission:
const providers = await trpc.gitProvider.getAll.query({});
console.log(providers[0].github?.githubPrivateKey?.startsWith("-----BEGIN")); // true
console.log(providers[0].github?.githubClientSecret?.length); // 40
console.log(providers[0].github?.githubWebhookSecret?.length); // 40
Or via the public MCP tool surface exposed by @dokploy/mcp: invoking mcp__dokploy-mcp__gitProvider-getAll returns the same leaked structure.
Root cause (source code reference)
In apps/dokploy/server/api/routers/git-provider.ts at v0.29.4 (commit b109e0e), the getAll procedure uses:
return await db.query.gitProvider.findMany({
with: { gitlab: true, bitbucket: true, github: true, gitea: true },
// ...
});
The with: { github: true } form returns every column of the related table, including the plaintext credential columns. The sibling procedure allForPermissions in the same router demonstrates the correct pattern:
return await db.query.gitProvider.findMany({
// ...
with: {
github: { columns: { /* explicit allowlist of safe columns only */ } },
// ...
},
});
Suggested fix
Mirror the allForPermissions pattern in getAll: add columns: {...} selectors to each with clause that explicitly excludes credential columns (githubPrivateKey, githubClientSecret, githubWebhookSecret, GitLab secret / accessToken / refreshToken, Bitbucket appPassword / apiToken, Gitea clientSecret / accessToken / refreshToken). The UI surface that consumes getAll does not appear to need these credential values — they're only used server-side for outbound API calls in worker handlers that read directly from the database.
Alternative (defense-in-depth): add a Drizzle schema-level select redactor middleware that strips fields tagged as secret from every query result outside of an explicit allow-list of callers.
Adjacent precedent
Discussion #3264 "Generated SSH Private Key Visible after generation" (resolved 2026-03-03) addressed the same systemic pattern in the SSH-key UI surface. The fix landed via a similar approach: hide the secret column from default reads.
Environment
- Dokploy: v0.29.4 (latest at time of report)
- Affected since: at least v0.26.5 (router file unchanged in the v0.26.5..v0.29.4 diff)
- Self-hosted (single-server Swarm mode)
- Reproduction was via
@dokploy/mcp@latest invoking the tRPC gitProvider-getAll tool
Severity
P1 — confidentiality breach, not RCE. The auth-required gate prevents anonymous exploitation, but any user with project-level admin access can effectively extract the keys to every connected git repo, persisted long-term and rotatable only via GitHub-side regeneration.
Acknowledgements
Reported by an external Dokploy user during their self-hosted production stand-up; reproducer and fix sketch produced through code archaeology against the v0.29.4 tag.
Summary
The
gitProvider.getAlltRPC procedure inapps/dokploy/server/api/routers/git-provider.tsreturns plaintext credentials (RSA private keys, OAuth client secrets, webhook secrets, access tokens) for every configured git provider (GitHub, GitLab, Bitbucket, Gitea) to any authenticated user with permission to list providers. This includes:githubPrivateKey(full RSA PEM-encoded private key),githubClientSecret(40-char hex),githubWebhookSecret(40-char hex)secret,accessToken,refreshToken(same shape inferred from schema)appPassword,apiTokenclientSecret,accessToken,refreshTokenVerified persists through v0.29.4 (latest release at time of report). The affected router file is unchanged between v0.26.5 and v0.29.4 per
gh api repos/Dokploy/dokploy/compare/v0.26.5...v0.29.4.Impact
Any user / API token with the
LISTpermission on git providers can extract the GitHub App private key. With the private key + clientId + installationId (also returned), an attacker can mint installation access tokens for any repo the App has access to — a privilege escalation from "can list git providers on this Dokploy instance" to "can push to / read from every repo connected to this Dokploy".Blast radius is amplified by the fact that the leaked tokens are long-lived (App-level, not session-scoped). Rotation requires manual GitHub-side regeneration.
Reproducer
Or via the public MCP tool surface exposed by
@dokploy/mcp: invokingmcp__dokploy-mcp__gitProvider-getAllreturns the same leaked structure.Root cause (source code reference)
In
apps/dokploy/server/api/routers/git-provider.tsat v0.29.4 (commitb109e0e), thegetAllprocedure uses:The
with: { github: true }form returns every column of the related table, including the plaintext credential columns. The sibling procedureallForPermissionsin the same router demonstrates the correct pattern:Suggested fix
Mirror the
allForPermissionspattern ingetAll: addcolumns: {...}selectors to eachwithclause that explicitly excludes credential columns (githubPrivateKey,githubClientSecret,githubWebhookSecret, GitLabsecret/accessToken/refreshToken, BitbucketappPassword/apiToken, GiteaclientSecret/accessToken/refreshToken). The UI surface that consumesgetAlldoes not appear to need these credential values — they're only used server-side for outbound API calls in worker handlers that read directly from the database.Alternative (defense-in-depth): add a Drizzle schema-level
selectredactor middleware that strips fields tagged assecretfrom every query result outside of an explicit allow-list of callers.Adjacent precedent
Discussion #3264 "Generated SSH Private Key Visible after generation" (resolved 2026-03-03) addressed the same systemic pattern in the SSH-key UI surface. The fix landed via a similar approach: hide the secret column from default reads.
Environment
@dokploy/mcp@latestinvoking the tRPCgitProvider-getAlltoolSeverity
P1 — confidentiality breach, not RCE. The auth-required gate prevents anonymous exploitation, but any user with project-level admin access can effectively extract the keys to every connected git repo, persisted long-term and rotatable only via GitHub-side regeneration.
Acknowledgements
Reported by an external Dokploy user during their self-hosted production stand-up; reproducer and fix sketch produced through code archaeology against the v0.29.4 tag.