mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-07 20:45:17 +00:00
Merge pull request #4857 from Dokploy/fix/idor-server-ssh-key-disclosure
fix(security): SSH private key disclosure via server read endpoints
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
import { redactServerSshKey } from "@dokploy/server/services/server";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("redactServerSshKey (server SSH private key disclosure guard)", () => {
|
||||||
|
it("blanks the private key while keeping the rest of the ssh key intact", () => {
|
||||||
|
const server = {
|
||||||
|
serverId: "srv-1",
|
||||||
|
name: "prod",
|
||||||
|
sshKey: {
|
||||||
|
sshKeyId: "key-1",
|
||||||
|
publicKey: "ssh-ed25519 AAAA...",
|
||||||
|
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const redacted = redactServerSshKey(server);
|
||||||
|
|
||||||
|
expect(redacted.sshKey.privateKey).toBe("");
|
||||||
|
// Non-secret fields and the surrounding record must survive untouched.
|
||||||
|
expect(redacted.sshKey.publicKey).toBe("ssh-ed25519 AAAA...");
|
||||||
|
expect(redacted.serverId).toBe("srv-1");
|
||||||
|
expect(redacted.name).toBe("prod");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mutate the original record", () => {
|
||||||
|
const server = {
|
||||||
|
serverId: "srv-1",
|
||||||
|
sshKey: { privateKey: "top-secret" },
|
||||||
|
};
|
||||||
|
redactServerSshKey(server);
|
||||||
|
expect(server.sshKey.privateKey).toBe("top-secret");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is a no-op when the server has no ssh key", () => {
|
||||||
|
const server = { serverId: "srv-2", sshKey: null };
|
||||||
|
expect(redactServerSshKey(server)).toEqual(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles a record without a loaded sshKey relation", () => {
|
||||||
|
// e.g. server.update returns the plain row where sshKey is not populated.
|
||||||
|
const server: { serverId: string; sshKey?: null } = { serverId: "srv-3" };
|
||||||
|
expect(redactServerSshKey(server)).toEqual(server);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
getPublicIpWithFallback,
|
getPublicIpWithFallback,
|
||||||
haveActiveServices,
|
haveActiveServices,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
|
redactServerSshKey,
|
||||||
removeDeploymentsByServerId,
|
removeDeploymentsByServerId,
|
||||||
serverAudit,
|
serverAudit,
|
||||||
serverSetup,
|
serverSetup,
|
||||||
@@ -105,7 +106,7 @@ export const serverRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return server;
|
return redactServerSshKey(server);
|
||||||
}),
|
}),
|
||||||
getDefaultCommand: withPermission("server", "read")
|
getDefaultCommand: withPermission("server", "read")
|
||||||
.input(apiFindOneServer)
|
.input(apiFindOneServer)
|
||||||
@@ -436,7 +437,7 @@ export const serverRouter = createTRPCRouter({
|
|||||||
await updateServersBasedOnQuantity(admin.id, admin.serversQuantity);
|
await updateServersBasedOnQuantity(admin.id, admin.serversQuantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentServer;
|
return redactServerSshKey(currentServer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,27 @@ export const findServerById = async (serverId: string) => {
|
|||||||
return currentServer;
|
return currentServer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the SSH private key material from a server record before it is sent
|
||||||
|
* to a client. `findServerById` eagerly loads the `sshKey` relation (needed for
|
||||||
|
* server-side SSH operations), but the private key must never leave the server:
|
||||||
|
* no client feature consumes it, and returning it exposed it to any member with
|
||||||
|
* only `server:read`. Server-side callers keep using `findServerById` directly.
|
||||||
|
*/
|
||||||
|
export const redactServerSshKey = <
|
||||||
|
T extends { sshKey?: { privateKey: string } | null },
|
||||||
|
>(
|
||||||
|
serverRecord: T,
|
||||||
|
): T => {
|
||||||
|
if (!serverRecord.sshKey) {
|
||||||
|
return serverRecord;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...serverRecord,
|
||||||
|
sshKey: { ...serverRecord.sshKey, privateKey: "" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const findServersByUserId = async (userId: string) => {
|
export const findServersByUserId = async (userId: string) => {
|
||||||
const orgs = await db.query.organization.findMany({
|
const orgs = await db.query.organization.findMany({
|
||||||
where: eq(organization.ownerId, userId),
|
where: eq(organization.ownerId, userId),
|
||||||
|
|||||||
Reference in New Issue
Block a user