[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
179
domain/sshConfigSerializer.test.ts
Normal file
179
domain/sshConfigSerializer.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type { Host } from "./models.ts";
|
||||
import { serializeHostsToSshConfig } from "./sshConfigSerializer.ts";
|
||||
import { importVaultHostsFromText } from "./vaultImport.ts";
|
||||
|
||||
const makeHost = (overrides: Partial<Host> = {}): Host => ({
|
||||
id: "host-1",
|
||||
label: "X11 Host",
|
||||
hostname: "x11.example.com",
|
||||
username: "root",
|
||||
port: 22,
|
||||
protocol: "ssh",
|
||||
os: "linux",
|
||||
tags: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig writes ForwardX11 for hosts with X11 forwarding enabled", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({ x11Forwarding: true })]);
|
||||
|
||||
assert.match(config, /ForwardX11 yes/);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig omits ForwardX11 when X11 forwarding is disabled", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({ x11Forwarding: false })]);
|
||||
|
||||
assert.doesNotMatch(config, /ForwardX11/);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig omits ForwardX11 for mosh hosts", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({ moshEnabled: true, x11Forwarding: true })]);
|
||||
|
||||
assert.doesNotMatch(config, /ForwardX11/);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig preserves system agent authentication directives", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({
|
||||
identityFilePaths: ["~/.ssh/aws_root"],
|
||||
useSshAgent: true,
|
||||
identityAgent: "$SSH_AUTH_SOCK",
|
||||
identitiesOnly: true,
|
||||
addKeysToAgent: "yes",
|
||||
useKeychain: true,
|
||||
})]);
|
||||
|
||||
assert.match(config, /IdentityFile ~\/\.ssh\/aws_root/);
|
||||
assert.match(config, /IdentityAgent \$SSH_AUTH_SOCK/);
|
||||
assert.match(config, /IdentitiesOnly yes/);
|
||||
assert.match(config, /AddKeysToAgent yes/);
|
||||
assert.match(config, /UseKeychain yes/);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig preserves a disabled imported agent setting", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({
|
||||
identityFilePaths: ["~/.ssh/aws_root"],
|
||||
useSshAgent: false,
|
||||
addKeysToAgent: "yes",
|
||||
useKeychain: true,
|
||||
})]);
|
||||
const imported = importVaultHostsFromText("ssh_config", config);
|
||||
|
||||
assert.match(config, /IdentityAgent none/);
|
||||
assert.notEqual(imported.hosts[0]?.useSshAgent, true);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig preserves a plain explicit agent opt-out", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({ useSshAgent: false })]);
|
||||
const imported = importVaultHostsFromText("ssh_config", config);
|
||||
|
||||
assert.match(config, /IdentityAgent none/);
|
||||
assert.equal(imported.hosts[0]?.useSshAgent, false);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig preserves an enabled default agent setting", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({ useSshAgent: true })]);
|
||||
const imported = importVaultHostsFromText("ssh_config", config);
|
||||
|
||||
assert.match(config, /IdentityAgent \$\{SSH_AUTH_SOCK\}/);
|
||||
assert.equal(imported.hosts[0]?.useSshAgent, true);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig rejects line injection in serialized fields", () => {
|
||||
const maliciousValues: Partial<Host>[] = [
|
||||
{ username: "root\nProxyCommand /tmp/run" },
|
||||
{ identityFilePaths: ["~/.ssh/id\rProxyCommand /tmp/run"] },
|
||||
{ hostname: "host.example.com\0ProxyCommand /tmp/run" },
|
||||
];
|
||||
|
||||
for (const overrides of maliciousValues) {
|
||||
assert.throws(
|
||||
() => serializeHostsToSshConfig([makeHost(overrides)]),
|
||||
/line breaks or null bytes/i,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig encodes Host pattern characters as literal aliases", () => {
|
||||
const encoded = serializeHostsToSshConfig([makeHost({ label: "prod*" })]);
|
||||
const literal = serializeHostsToSshConfig([makeHost({ label: "prod-2a-" })]);
|
||||
const leadingDash = serializeHostsToSshConfig([makeHost({ label: "-jump" })]);
|
||||
const quoted = serializeHostsToSshConfig([makeHost({ label: 'bad"alias' })]);
|
||||
const escaped = serializeHostsToSshConfig([makeHost({ label: 'bad\\alias' })]);
|
||||
|
||||
assert.match(encoded, /^Host netcatty-encoded-/m);
|
||||
assert.match(literal, /^Host prod-2a-$/m);
|
||||
assert.notEqual(encoded.match(/^Host (.+)$/m)?.[1], literal.match(/^Host (.+)$/m)?.[1]);
|
||||
assert.match(leadingDash, /^Host netcatty-encoded-/m);
|
||||
assert.doesNotMatch(leadingDash, /^Host -/m);
|
||||
assert.match(quoted, /^Host netcatty-encoded-/m);
|
||||
assert.doesNotMatch(quoted, /^Host .*"/m);
|
||||
assert.match(escaped, /^Host netcatty-encoded-/m);
|
||||
assert.doesNotMatch(escaped, /^Host .*\\/m);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig quotes usernames containing spaces", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({ username: "alice smith" })]);
|
||||
|
||||
assert.match(config, /^ {4}User "alice smith"$/m);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig escapes quoted SSH arguments", () => {
|
||||
const config = serializeHostsToSshConfig([makeHost({
|
||||
hostname: 'bad"host',
|
||||
username: 'alice"ops',
|
||||
identityFilePaths: ['~/.ssh/id"quoted', '~/.ssh/id\\backslash'],
|
||||
identityAgent: '/tmp/agent"socket',
|
||||
})]);
|
||||
|
||||
assert.match(config, /^ {4}HostName "bad\\"host"$/m);
|
||||
assert.match(config, /^ {4}User "alice\\"ops"$/m);
|
||||
assert.match(config, /^ {4}IdentityFile "~\/\.ssh\/id\\"quoted"$/m);
|
||||
assert.match(config, /^ {4}IdentityFile "~\/\.ssh\/id\\\\backslash"$/m);
|
||||
assert.match(config, /^ {4}IdentityAgent "\/tmp\/agent\\"socket"$/m);
|
||||
});
|
||||
|
||||
test("serializeHostsToSshConfig rejects ProxyJump separator injection", () => {
|
||||
const target = makeHost({
|
||||
id: "target",
|
||||
hostChain: { hostIds: ["jump"] },
|
||||
});
|
||||
const jump = makeHost({
|
||||
id: "jump",
|
||||
hostname: "legit.example,attacker.example",
|
||||
});
|
||||
const badUsernameJump = makeHost({
|
||||
id: "jump",
|
||||
hostname: "jump.example.com",
|
||||
username: "root,attacker",
|
||||
});
|
||||
const emailUsernameJump = makeHost({
|
||||
id: "jump",
|
||||
hostname: "jump.example.com",
|
||||
username: "alice@example.com",
|
||||
});
|
||||
const optionLikeJump = makeHost({
|
||||
id: "jump",
|
||||
hostname: "-oProxyCommand=run",
|
||||
username: "",
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => serializeHostsToSshConfig([target], [target, jump]),
|
||||
/ProxyJump separator/i,
|
||||
);
|
||||
assert.throws(
|
||||
() => serializeHostsToSshConfig([target], [target, badUsernameJump]),
|
||||
/ProxyJump separator/i,
|
||||
);
|
||||
assert.match(
|
||||
serializeHostsToSshConfig([target], [target, emailUsernameJump]),
|
||||
/ProxyJump alice@example\.com@jump\.example\.com/,
|
||||
);
|
||||
assert.throws(
|
||||
() => serializeHostsToSshConfig([target], [target, optionLikeJump]),
|
||||
/ProxyJump separator/i,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user