Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions src/utils/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,13 @@ fi
`Could not detect your shell profile. To use localstack CLI from terminal ensure ~/.local/bin is in your PATH.`,
);
}

// Update PATH for the current VSCode process so LocalStack is immediately available
const localBinPath = path.join(homeDir, ".local", "bin");
const currentPath = process.env.PATH || "";
if (!currentPath.split(":").includes(localBinPath)) {
process.env.PATH = `${localBinPath}:${currentPath}`;
}
} catch (err) {
window.showInformationMessage(
`Could not update your shell profile. To use localstack CLI from terminal ensure ~/.local/bin is in your PATH.`,
Expand Down Expand Up @@ -515,9 +522,6 @@ async function installLocalWindows(temporaryDirname: string) {
await move(`${temporaryDirname}/localstack`, LOCAL_CLI_INSTALLATION_DIRNAME);
await exec(`setx PATH "%PATH%;${LOCAL_CLI_INSTALLATION_DIRNAME}"`);

// // Update PATH for current process (setx only updates for new processes, including vscode)
// process.env.PATH = `${process.env.PATH};${CLI_INSTALLATION_DIRNAME}`;

window.showInformationMessage("LocalStack CLI installed for current user.");
}

Expand Down Expand Up @@ -551,17 +555,23 @@ async function installGlobalLinux(
) {
// Use elevated privileges to install globally on linux

const { cancelled } = await spawnElevatedLinux({
//TODO consider loading script from a file
script: `rm -rf /usr/local/localstack && mv ${temporaryDirname}/localstack /usr/local/localstack && ln -sf /usr/local/localstack/localstack /usr/local/bin/localstack`,
outputChannel,
outputLabel: "install",
cancellationToken,
});
if (cancelled) {
//TODO check if progress can be used instead of window
window.showErrorMessage("The installation was cancelled by the user");
return { cancelled: true };
try {
const { cancelled } = await spawnElevatedLinux({
//TODO consider loading script from a file
script: `rm -rf /usr/local/localstack && mv ${temporaryDirname}/localstack /usr/local/localstack && ln -sf /usr/local/localstack/localstack /usr/local/bin/localstack`,
outputChannel,
outputLabel: "install",
cancellationToken,
});
if (cancelled) {
//TODO check if progress can be used instead of window
window.showErrorMessage("The installation was cancelled by the user");
return { cancelled: true };
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
window.showErrorMessage(`Installation failed: ${message}`);
throw error;
}
}

Expand Down
132 changes: 121 additions & 11 deletions src/utils/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as childProcess from "node:child_process";
import { appendFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";

import * as vscode from "vscode";
import type { CancellationToken, LogOutputChannel } from "vscode";

import { spawn, SpawnError } from "./spawn.ts";
import { pipeToLogOutputChannel, spawn, SpawnError } from "./spawn.ts";

export interface SpawnElevatedDarwinOptions {
script: string;
Expand Down Expand Up @@ -50,28 +52,136 @@ export interface SpawnElevatedLinuxOptions {
cancellationToken?: CancellationToken;
}

export async function spawnElevatedLinux(
async function tryPkexec(
options: SpawnElevatedLinuxOptions,
): Promise<{ cancelled: boolean }> {
): Promise<{ success: boolean; cancelled: boolean }> {
const scriptArg = JSON.stringify(options.script);
try {
await spawn("pkexec", ["sh", "-c", `${JSON.stringify(options.script)}`], {
await spawn("pkexec", ["sh", "-c", scriptArg], {
outputChannel: options.outputChannel,
outputLabel: options.outputLabel,
cancellationToken: options.cancellationToken,
});
return {
cancelled: false,
};
return { success: true, cancelled: false };
} catch (error) {
if (error instanceof SpawnError && error.code === 126) {
return { cancelled: true };
if (error instanceof SpawnError) {
// User cancelled (pkexec returns 126)
if (error.code === 126) {
return { success: true, cancelled: true };
}
// Other failures (no polkit agent, TTY error, etc.) - signal to try fallback
return { success: false, cancelled: false };
}

options.outputChannel.error(error instanceof Error ? error : String(error));
throw error;
}
}

async function spawnWithSudoPassword(
options: SpawnElevatedLinuxOptions,
): Promise<{ cancelled: boolean }> {
// Security note: This approach is less secure than pkexec because:
// - Password flows through VS Code → Node.js → child process stdin (more attack surface)
// - Password briefly exists in application memory (vulnerable to memory dumps)
// - Potential attack vector: A malicious VS Code extension could theoretically intercept
// the password by hooking into Node.js I/O streams or reading process memory
// (though VS Code's extension sandbox provides some isolation)
//
// However, this is acceptable because:
// - We only use this when pkexec is unavailable (systems without desktop environment)
// - Password is masked in input, never logged, and passed via stdin only
// - Password is not stored/cached and is disposed immediately
// - This is a one-time installation operation, not continuous privileged access
//
// Alternative: complete installation failure on systems without polkit agents
const password = await vscode.window.showInputBox({
prompt: "Enter your sudo password",
password: true,
ignoreFocusOut: true,
});

if (password === undefined) {
return { cancelled: true };
}

const outputLabel = options.outputLabel ? `[${options.outputLabel}]: ` : "";

return new Promise((resolve, reject) => {
let stderrOutput = "";

const child = childProcess.spawn(
"sudo",
["-S", "sh", "-c", options.script],
{
stdio: ["pipe", "pipe", "pipe"],
},
);

// Capture stderr to detect wrong password
child.stderr?.on("data", (data: Buffer) => {
stderrOutput += data.toString();
});

// Write password to stdin and close it
// Note: We use stdin rather than command-line arguments because:
// - Command-line args are visible in process listings (eg ps aux)
// - stdin prevents the password from appearing in logs or shell history
// - stdin data is not visible to other users on the system
// However, the password still exists briefly in our process memory and the pipe buffer
child.stdin.write(`${password}\n`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: nice way to avoid exposing password in any kind of output! 👍

child.stdin.end();

// Redirect the child process stdout/stderr to VSCode's output channel for visibility
pipeToLogOutputChannel(child, options.outputChannel, outputLabel);

const disposeCancel = options.cancellationToken?.onCancellationRequested(
() => {
child.kill("SIGINT");
reject(new Error("Command cancelled"));
},
);

child.on("close", (code) => {
disposeCancel?.dispose();
if (code === 0) {
resolve({ cancelled: false });
} else {
// Detect wrong password from stderr
const isWrongPassword =
stderrOutput.includes("Sorry, try again") ||
stderrOutput.includes("incorrect password") ||
stderrOutput.includes("Authentication failure");

const errorMessage = isWrongPassword
? "Incorrect sudo password"
: `sudo command failed with exit code ${code}`;

reject(new Error(errorMessage));
}
});

child.on("error", (error) => {
disposeCancel?.dispose();
reject(error);
});
});
}

export async function spawnElevatedLinux(
options: SpawnElevatedLinuxOptions,
): Promise<{ cancelled: boolean }> {
// Try pkexec first (works with graphical polkit agents)
const pkexecResult = await tryPkexec(options);
if (pkexecResult.success) {
return { cancelled: pkexecResult.cancelled };
}

// Fallback: prompt for sudo password via VS Code input
options.outputChannel.info(
"pkexec not available, falling back to sudo password prompt",
);
return spawnWithSudoPassword(options);
}

export async function spawnElevatedWindows({
script,
outputChannel,
Expand Down
Loading