From b3e220acf183c648326a60e2ce82a7de5ef0bdc3 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sat, 3 Oct 2026 16:35:09 -0500 Subject: [PATCH] fix: host key silently accepted when the host is missing from the database (#1397) --- src/backend/hosts/host-key-verifier.ts | 33 +++++++++++++- .../tests/hosts/host-key-recovery.test.ts | 44 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/backend/hosts/host-key-verifier.ts b/src/backend/hosts/host-key-verifier.ts index fb7158f25..375260568 100644 --- a/src/backend/hosts/host-key-verifier.ts +++ b/src/backend/hosts/host-key-verifier.ts @@ -84,18 +84,47 @@ export class SSHHostKeyVerifier { hostId, ); + // No row means nothing to compare against and nowhere to save the + // key, so it is never accepted silently (#1397): the user decides + // when there is a socket to ask on, otherwise it is refused. if (!host) { + if (!ws) { + sshLogger.error( + "Host not found in database during key verification, rejecting", + { + operation: "host_key_no_host", + hostId, + ip, + port, + userId, + }, + ); + verify(false); + return; + } + const accepted = await this.promptUserForNewKey( + ws, + ip, + port, + undefined, + fingerprint, + keyType, + algorithm, + ); sshLogger.warn( - "Host not found in database during key verification", + accepted + ? "Key for a host missing from the database accepted for this session only" + : "Key for a host missing from the database rejected", { operation: "host_key_no_host", hostId, ip, port, + fingerprint, userId, }, ); - verify(true); + verify(accepted); return; } diff --git a/src/backend/tests/hosts/host-key-recovery.test.ts b/src/backend/tests/hosts/host-key-recovery.test.ts index 61e0b0ea7..1a09f8269 100644 --- a/src/backend/tests/hosts/host-key-recovery.test.ts +++ b/src/backend/tests/hosts/host-key-recovery.test.ts @@ -76,3 +76,47 @@ describe("host key recovery", () => { expect(accepted).toEqual([]); }); }); + +describe("a host missing from the database", () => { + function answeringSocket(action: "accept" | "reject") { + const socket = new EventEmitter() as EventEmitter & { + send: (data: string) => void; + }; + socket.send = () => + socket.emit( + "message", + Buffer.from( + JSON.stringify({ + type: "host_key_verification_response", + data: { action }, + }), + ), + ); + return socket as unknown as WebSocket; + } + + async function verifyMissingHost(ws: WebSocket | null) { + const verifier = await SSHHostKeyVerifier.createHostVerifier( + hostId, + "127.0.0.1", + 22, + ws, + "user", + false, + null, + ); + return new Promise((resolve) => + verifier(Buffer.from("any-key"), resolve), + ); + } + + it("refuses the key when nobody can be asked", async () => { + expect(await verifyMissingHost(null)).toBe(false); + }); + + it("asks the user and saves nothing", async () => { + expect(await verifyMissingHost(answeringSocket("accept"))).toBe(true); + expect(await verifyMissingHost(answeringSocket("reject"))).toBe(false); + expect(updateHostKey).not.toHaveBeenCalled(); + }); +});