Skip to content

[BUG] 2.7.1: Host key verification still uses the client's numeric id — wrong host's key is compared and overwritten, breaking pinning on a sync server #1277

Description

@chtowsappx

2.7.1: Host key verification still uses the client's numeric id — wrong host's key is compared and overwritten, breaking host key pinning on a sync server

Platform

App - macOS (Apple Silicon) desktop against a self-hosted server; the code path is server-side, so every client using "Remote server" origin is affected

Server Installation Method

Docker

Version

Desktop app 2.7.1; self-hosted server 2.7.1

Troubleshooting

  • I have examined logs and tried to find the issue
  • I have reviewed opened and closed issues
  • I have tried restarting the application
  • I have checked open issues and ensured this is not a duplicate

The Problem

Follow-up to #1092 / Termix#1178. The connection itself is now correctly resolved by syncId, but host key verification was missed and still uses the raw client-supplied numeric id. On a sync server that id names a different host, so Termix compares the connected machine's key against another host's stored key, shows that other host in the prompt, and — when the user accepts — writes the key into that other host's row.

In src/backend/hosts/terminal/index.ts the resolution is correct:

resolvedHostData = (hostSyncId
    ? await resolveHostBySyncId(hostSyncId, userId)
    : await resolveHostById(id, userId));

but id is never reassigned to resolvedHostData.id, and the two host-key calls keep using it:

const preloadedHostData = await SSHHostKeyVerifier.preloadHostData(id);          // ~line 2130
...
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(id, ip, port, ws, userId, false, preloadedHostData),  // ~line 2146

ip/port/credentials/jump hosts are all taken from resolvedHostData, so the session goes to the right machine — only the identity check goes to the wrong row.

Reproduced with exact data. My two databases are offset by one for these hosts:

Host Desktop id Server id Real ed25519 key (hex prefix, via ssh-keyscan)
100 File Browser (192.168.1.100) 3 4 …2a77818992bacbce9e167e…
103 AdGuard (192.168.1.103) 4 5 …40a82d2487ec2f7a54653c…

Opening a terminal to 103 AdGuard (desktop id 4) produces a host key prompt titled "100 File Browser:22", showing File Browser's stored key as "previous key" and AdGuard's key as "new fingerprint" — because server row 4 is File Browser.

After accepting, the server rows read:

id=4  100 File Browser  192.168.1.100  fp=…40a82d2487ec…  changed=1   <-- AdGuard's key, wrong
id=5  103 AdGuard       192.168.1.103  fp=…40a82d2487ec…  changed=0

File Browser is now pinned to AdGuard's key.

Consequences:

  • Host key pinning is effectively disabled. Every host is verified against a neighbouring host's pin, so a genuine key change — the thing pinning exists to catch — cannot be distinguished from this bug.
  • Accepting corrupts a different host's pin, which makes that host prompt on its next connection. The user sees an endless series of "key changed" prompts that never stick ("I accept it, reconnect, and it asks again"), and each acceptance damages another row.
  • Users are trained to click through host key warnings, which is the exact opposite of what the dialog is for.
  • The prompt's "previous key" belongs to an unrelated machine, so a user who tries to verify the fingerprint out-of-band cannot make sense of it.

Note this is also why hosts sit at Reachable with no explanation (#1266): the metrics collector is refused by the same mismatched comparison.

How to Reproduce

  1. Desktop + self-hosted server, Remote Sync on, connection origin "Remote server", and host row ids that differ between the two databases (any sync history produces this).
  2. Open a terminal to a host whose server-side id differs from its desktop id.
  3. The host key prompt names a different host and shows that host's stored key as "previous".
  4. Accept it: the key is written to the other host's row (hostKeyChangedCount increments there), the connected host's own row is untouched, and the prompts keep coming.

Additional Context

Suggested fix: use the resolved row's id for the host-key path, e.g. reassign after resolution

const effectiveHostId = resolvedHostData?.id ?? id;

and pass effectiveHostId to both preloadHostData() and createHostVerifier(). A regression test asserting that the verifier is constructed with the resolved id (not the client id) when the two differ would lock this down.

Worth auditing the same pattern in the other host-key call sites (hosts/metrics/index.ts builds its verifier from host.id, which is server-side and therefore fine, but the file-manager, docker and tunnel paths deserve a look).

Also: consider giving users a way to reset a host's stored key from the UI. Right now hostKeyFingerprint is written only by the verifier, so a row corrupted this way cannot be repaired from the app.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions