diff --git a/src/steps/ssh.js b/src/steps/ssh.js index ead1905..80dc24d 100644 --- a/src/steps/ssh.js +++ b/src/steps/ssh.js @@ -42,8 +42,8 @@ export async function setupSsh({ home } = {}) { p.log.step("Generating SSH key..."); await runStream( - `ssh-keygen -t rsa -b 4096 -C "${email}" -f "${keyFile}" -N "$SSH_KEYGEN_PASSPHRASE"`, - { env: { ...process.env, SSH_KEYGEN_PASSPHRASE: passphrase } } + `ssh-keygen -t rsa -b 4096 -C "$SSH_KEYGEN_EMAIL" -f "${keyFile}" -N "$SSH_KEYGEN_PASSPHRASE"`, + { env: { ...process.env, SSH_KEYGEN_EMAIL: email, SSH_KEYGEN_PASSPHRASE: passphrase } } ); // Copy public key to clipboard diff --git a/tests/ssh.test.js b/tests/ssh.test.js index b7a54d8..87af9f8 100644 --- a/tests/ssh.test.js +++ b/tests/ssh.test.js @@ -59,7 +59,9 @@ describe("ssh step", () => { expect(mockText).toHaveBeenCalled(); const sshKeygenCall = runStream.mock.calls.find((c) => c[0].includes("ssh-keygen")); expect(sshKeygenCall).toBeDefined(); - expect(sshKeygenCall[0]).toContain("test@example.com"); + expect(sshKeygenCall[0]).not.toContain("test@example.com"); + expect(sshKeygenCall[0]).toContain('-C "$SSH_KEYGEN_EMAIL"'); + expect(sshKeygenCall[1]).toEqual({ env: expect.objectContaining({ SSH_KEYGEN_EMAIL: "test@example.com" }) }); }); test("uses sandbox path for key file location", async () => { @@ -78,7 +80,10 @@ describe("ssh step", () => { const sshKeygenCall = runStream.mock.calls.find((c) => c[0].includes("ssh-keygen")); expect(sshKeygenCall).toBeDefined(); expect(sshKeygenCall[0]).toContain('-N "$SSH_KEYGEN_PASSPHRASE"'); - expect(sshKeygenCall[1]).toEqual({ env: expect.objectContaining({ SSH_KEYGEN_PASSPHRASE: "" }) }); + expect(sshKeygenCall[0]).toContain('-C "$SSH_KEYGEN_EMAIL"'); + expect(sshKeygenCall[1]).toEqual({ + env: expect.objectContaining({ SSH_KEYGEN_PASSPHRASE: "", SSH_KEYGEN_EMAIL: "test@example.com" }), + }); }); test("generates key with passphrase when provided", async () => { @@ -89,7 +94,10 @@ describe("ssh step", () => { const sshKeygenCall = runStream.mock.calls.find((c) => c[0].includes("ssh-keygen")); expect(sshKeygenCall).toBeDefined(); expect(sshKeygenCall[0]).toContain('-N "$SSH_KEYGEN_PASSPHRASE"'); - expect(sshKeygenCall[1]).toEqual({ env: expect.objectContaining({ SSH_KEYGEN_PASSPHRASE: "s3cr3tPass" }) }); + expect(sshKeygenCall[0]).toContain('-C "$SSH_KEYGEN_EMAIL"'); + expect(sshKeygenCall[1]).toEqual({ + env: expect.objectContaining({ SSH_KEYGEN_PASSPHRASE: "s3cr3tPass", SSH_KEYGEN_EMAIL: "test@example.com" }), + }); }); test("aborts when passphrase prompt is cancelled", async () => { @@ -110,4 +118,18 @@ describe("ssh step", () => { expect.objectContaining({ message: expect.stringContaining("passphrase") }) ); }); + + test("does not interpolate email into command string (prevents shell injection)", async () => { + mockText.mockResolvedValue('evil@example.com; echo "injected"'); + + await setupSsh({ home: sandbox.path }); + + const sshKeygenCall = runStream.mock.calls.find((c) => c[0].includes("ssh-keygen")); + expect(sshKeygenCall).toBeDefined(); + expect(sshKeygenCall[0]).not.toContain("evil@example.com"); + expect(sshKeygenCall[0]).not.toContain("injected"); + expect(sshKeygenCall[1]).toEqual({ + env: expect.objectContaining({ SSH_KEYGEN_EMAIL: 'evil@example.com; echo "injected"' }), + }); + }); });