/**
* Tests for SSH username validation (S-M3)
*
* SECURITY (CWE-250): Prevents execution with unnecessary privileges by
* requiring explicit SSH username configuration instead of defaulting to root.
*/
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { HostConfig } from "../types.js";
import { SSHConnectionPoolImpl } from "./ssh-pool.js";
describe("SSH username validation (S-M3)", () => {
let originalUSER: string | undefined;
beforeEach(() => {
originalUSER = process.env.USER;
});
afterEach(() => {
if (originalUSER !== undefined) {
process.env.USER = originalUSER;
} else {
delete process.env.USER;
}
});
describe("explicit sshUser configuration", () => {
it("should use sshUser when provided", async () => {
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshUser: "myuser",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl();
// This will fail to actually connect, but we can check the error doesn't mention username
await expect(pool.getConnection(host)).rejects.toThrow();
// The connection attempt should have tried with "myuser" (we can't verify this directly
// without mocking node-ssh, but the test ensures the code path works)
});
it("should prefer sshUser over USER environment variable", async () => {
process.env.USER = "envuser";
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshUser: "configuser",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl();
// Connection will fail but should not error about username
await expect(pool.getConnection(host)).rejects.toThrow();
});
});
describe("USER environment variable fallback", () => {
it("should use USER env var when sshUser not provided", async () => {
process.env.USER = "envuser";
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl();
// Connection will fail but should not error about username
await expect(pool.getConnection(host)).rejects.toThrow();
});
});
describe("no username available - should fail", () => {
it("should throw clear error when neither sshUser nor USER is available", async () => {
delete process.env.USER;
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl();
await expect(pool.getConnection(host)).rejects.toThrow(
/No SSH username configured for host testhost/
);
});
it("should include helpful message about setting sshUser or USER", async () => {
delete process.env.USER;
const host: HostConfig = {
name: "myserver",
host: "example.com",
protocol: "ssh",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl();
await expect(pool.getConnection(host)).rejects.toThrow(
/Set sshUser in config or USER env var/
);
});
it("should fail before attempting SSH connection", async () => {
delete process.env.USER;
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl();
// Should throw username error immediately, not connection error
try {
await pool.getConnection(host);
expect.fail("Should have thrown username error");
} catch (error) {
expect(error).toBeInstanceOf(Error);
if (error instanceof Error) {
expect(error.message).toMatch(/No SSH username configured/);
// Should NOT be an SSH connection error
expect(error.message).not.toMatch(/connection failed/i);
}
}
});
});
describe("security: no root fallback", () => {
it("should NOT default to root when username is missing", async () => {
delete process.env.USER;
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl();
// Should fail with username error, not attempt connection as root
await expect(pool.getConnection(host)).rejects.toThrow(/No SSH username configured/);
});
});
describe("security: reject root username (CWE-250)", () => {
it.skip("should reject root as sshUser", async () => {
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshUser: "root",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl({ enableHealthChecks: false });
await expect(pool.getConnection(host)).rejects.toThrow(/SSH as root is not allowed/);
await pool.closeAll();
});
it.skip("should reject root from USER env var fallback", async () => {
process.env.USER = "root";
const host: HostConfig = {
name: "testhost",
host: "192.168.1.100",
protocol: "ssh",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl({ enableHealthChecks: false });
await expect(pool.getConnection(host)).rejects.toThrow(/SSH as root is not allowed/);
await pool.closeAll();
});
it.skip("should suggest using non-root user with sudo", async () => {
const host: HostConfig = {
name: "myserver",
host: "example.com",
protocol: "ssh",
sshUser: "root",
sshKeyPath: "/tmp/fake-key",
};
const pool = new SSHConnectionPoolImpl({ enableHealthChecks: false });
await expect(pool.getConnection(host)).rejects.toThrow(
/Use a non-root user with appropriate sudo permissions/
);
await pool.closeAll();
});
});
});