Skip to content

Commit 5e3e13e

Browse files
committed
feat(ssh): add SSH host key verification to prevent MitM attacks
1 parent f6281d6 commit 5e3e13e

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

src/proxy/ssh/knownHosts.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Default SSH host keys for common Git hosting providers
3+
*
4+
* These fingerprints are the SHA256 hashes of the ED25519 host keys.
5+
* They should be verified against official documentation periodically.
6+
*
7+
* Sources:
8+
* - GitHub: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
9+
* - GitLab: https://docs.gitlab.com/ee/user/gitlab_com/
10+
*/
11+
12+
export interface KnownHostsConfig {
13+
[hostname: string]: string;
14+
}
15+
16+
/**
17+
* Default known host keys for GitHub and GitLab
18+
* Last updated: 2025-01-26
19+
*/
20+
export const DEFAULT_KNOWN_HOSTS: KnownHostsConfig = {
21+
'github.com': 'SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU',
22+
'gitlab.com': 'SHA256:eUXGGm1YGsMAS7vkcx6JOJdOGHPem5gQp4taiCfCLB8',
23+
};
24+
25+
/**
26+
* Get known hosts configuration with defaults merged
27+
*/
28+
export function getKnownHosts(customHosts?: KnownHostsConfig): KnownHostsConfig {
29+
return {
30+
...DEFAULT_KNOWN_HOSTS,
31+
...(customHosts || {}),
32+
};
33+
}
34+
35+
/**
36+
* Verify a host key fingerprint against known hosts
37+
*
38+
* @param hostname The hostname being connected to
39+
* @param keyHash The SSH key fingerprint (e.g., "SHA256:abc123...")
40+
* @param knownHosts Known hosts configuration
41+
* @returns true if the key matches, false otherwise
42+
*/
43+
export function verifyHostKey(
44+
hostname: string,
45+
keyHash: string,
46+
knownHosts: KnownHostsConfig,
47+
): boolean {
48+
const expectedKey = knownHosts[hostname];
49+
50+
if (!expectedKey) {
51+
console.error(`[SSH] Host key verification failed: Unknown host '${hostname}'`);
52+
console.error(` Add the host key to your configuration:`);
53+
console.error(` "ssh": { "knownHosts": { "${hostname}": "SHA256:..." } }`);
54+
return false;
55+
}
56+
57+
if (keyHash !== expectedKey) {
58+
console.error(`[SSH] Host key verification failed for '${hostname}'`);
59+
console.error(` Expected: ${expectedKey}`);
60+
console.error(` Received: ${keyHash}`);
61+
console.error(` `);
62+
console.error(` WARNING: This could indicate a man-in-the-middle attack!`);
63+
console.error(` If the host key has legitimately changed, update your configuration.`);
64+
return false;
65+
}
66+
67+
return true;
68+
}

src/proxy/ssh/sshHelpers.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ import { getProxyUrl, getSSHConfig } from '../../config';
22
import { KILOBYTE, MEGABYTE } from '../../constants';
33
import { ClientWithUser } from './types';
44
import { createLazyAgent } from './AgentForwarding';
5+
import { getKnownHosts, verifyHostKey } from './knownHosts';
6+
import * as crypto from 'crypto';
7+
8+
/**
9+
* Calculate SHA-256 fingerprint from SSH host key Buffer
10+
*/
11+
function calculateHostKeyFingerprint(keyBuffer: Buffer): string {
12+
const hash = crypto.createHash('sha256').update(keyBuffer).digest('base64');
13+
// Remove base64 padding to match SSH fingerprint standard format
14+
const hashWithoutPadding = hash.replace(/=+$/, '');
15+
return `SHA256:${hashWithoutPadding}`;
16+
}
517

618
/**
719
* Default error message for missing agent forwarding
@@ -53,6 +65,8 @@ export function createSSHConnectionOptions(
5365

5466
const remoteUrl = new URL(proxyUrl);
5567
const customAgent = createLazyAgent(client);
68+
const sshConfig = getSSHConfig();
69+
const knownHosts = getKnownHosts(sshConfig?.knownHosts);
5670

5771
const connectionOptions: any = {
5872
host: remoteUrl.hostname,
@@ -61,6 +75,22 @@ export function createSSHConnectionOptions(
6175
tryKeyboard: false,
6276
readyTimeout: 30000,
6377
agent: customAgent,
78+
hostVerifier: (keyHash: Buffer | string, callback: (valid: boolean) => void) => {
79+
const hostname = remoteUrl.hostname;
80+
81+
// ssh2 passes the raw key as a Buffer, calculate SHA256 fingerprint
82+
const fingerprint = Buffer.isBuffer(keyHash) ? calculateHostKeyFingerprint(keyHash) : keyHash;
83+
84+
console.log(`[SSH] Verifying host key for ${hostname}: ${fingerprint}`);
85+
86+
const isValid = verifyHostKey(hostname, fingerprint, knownHosts);
87+
88+
if (isValid) {
89+
console.log(`[SSH] Host key verification successful for ${hostname}`);
90+
}
91+
92+
callback(isValid);
93+
},
6494
};
6595

6696
if (options?.keepalive) {

0 commit comments

Comments
 (0)