mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix: stop auto-accepting changed jump host keys (#1620)
A jump hop whose key no longer matched the pinned fingerprint was silently accepted and the pin overwritten. Background status probes reconnect to jump hosts every cycle, so a MITM on that hop got the credentials with no user interaction. Changed jump host keys now go through the same path as any other host: prompt when there is a terminal socket, otherwise refuse.
This commit is contained in:
1 parent
fb04edc497
commit
d791e52d82
2 files changed
+55
-24
No files matched your search
@@ -263,28 +263,6 @@ export class SSHHostKeyVerifier {
|
||||
changeCount: host.hostKeyChangedCount || 0,
|
||||
});
|
||||
|
||||
if (isJumpHost) {
|
||||
await this.updateHostKey(
|
||||
hostId,
|
||||
fingerprint,
|
||||
keyType,
|
||||
algorithm,
|
||||
host.hostKeyChangedCount || 0,
|
||||
);
|
||||
sshLogger.warn("Jump host key changed - auto-accepted", {
|
||||
operation: "host_key_updated",
|
||||
hostId,
|
||||
ip,
|
||||
port,
|
||||
fingerprint,
|
||||
keyType,
|
||||
userId,
|
||||
isJumpHost: true,
|
||||
});
|
||||
verify(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ws) {
|
||||
sshLogger.error(
|
||||
"Host key changed - please connect via Terminal to verify the new key",
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { WebSocket } from "ws";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SSHHostKeyVerifier } from "../../hosts/host-key-verifier.js";
|
||||
|
||||
const repo = vi.hoisted(() => ({
|
||||
updateHostKey: vi.fn(async () => {}),
|
||||
storeHostKey: vi.fn(async () => {}),
|
||||
touchHostKeyLastVerified: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentHostResolutionRepository: () => ({}),
|
||||
createCurrentHostResolutionRepository: () => repo,
|
||||
}));
|
||||
vi.mock("../../utils/logger.js", () => ({
|
||||
sshLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
@@ -57,3 +63,50 @@ describe("quick connect host keys", () => {
|
||||
expect(await verifyUnsaved(null)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
const pinned = {
|
||||
hostKeyFingerprint: Buffer.from("old-key").toString("hex"),
|
||||
hostKeyType: "ssh-ed25519",
|
||||
hostKeyAlgorithm: "sha256",
|
||||
hostKeyChangedCount: 0,
|
||||
name: "bastion",
|
||||
};
|
||||
|
||||
async function verifyJumpHop(socket: WebSocket | null, key: string) {
|
||||
const verifier = await SSHHostKeyVerifier.createHostVerifier(
|
||||
7,
|
||||
"10.0.0.1",
|
||||
22,
|
||||
socket,
|
||||
"user",
|
||||
true,
|
||||
pinned,
|
||||
);
|
||||
return new Promise<boolean>((resolve) => verifier(Buffer.from(key), resolve));
|
||||
}
|
||||
|
||||
describe("jump host key changes", () => {
|
||||
beforeEach(() => {
|
||||
repo.updateHostKey.mockClear();
|
||||
});
|
||||
|
||||
it("refuses a changed key and keeps the pin when nobody can be asked", async () => {
|
||||
expect(await verifyJumpHop(null, "new-key")).toBe(false);
|
||||
expect(repo.updateHostKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("asks the user and only replaces the pin when they accept", async () => {
|
||||
const no = socketAnswering("reject");
|
||||
expect(await verifyJumpHop(no.socket, "new-key")).toBe(false);
|
||||
expect(no.sent[0]).toContain("host_key_changed");
|
||||
expect(repo.updateHostKey).not.toHaveBeenCalled();
|
||||
|
||||
const yes = socketAnswering("accept");
|
||||
expect(await verifyJumpHop(yes.socket, "new-key")).toBe(true);
|
||||
expect(repo.updateHostKey).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("still accepts the pinned key without asking", async () => {
|
||||
expect(await verifyJumpHop(null, "old-key")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user