decolua/9router · error · Error
Invalid sudo password
Error message
Invalid sudo password
What it means
installTailscaleLinux() pipes the sudo password to child processes over stdin, so a password containing a newline would terminate the sudo prompt early and inject an attacker-chosen command. As a security measure it validates the password is a string without '\n' and throws 'Invalid sudo password' otherwise, refusing to install.
Source
Thrown at src/lib/tunnel/tailscale/tailscale.js:372
try { execSync(`rm -f ${pkgPath}`, { stdio: "ignore", windowsHide: true }); } catch { /* ignore */ }
if (c === 0) resolve();
else {
const msg = (stderr.includes("incorrect password") || stderr.includes("Sorry"))
? "Wrong sudo password"
: stderr || `Exit code ${c}`;
reject(new Error(msg));
}
});
child.on("error", reject);
child.stdin.write(`${sudoPassword}\n`);
child.stdin.end();
});
}
async function installTailscaleLinux(sudoPassword, log) {
// Reject password containing newline → prevents stdin command injection
if (typeof sudoPassword !== "string" || sudoPassword.includes("\n")) {
throw new Error("Invalid sudo password");
}
log("Downloading install script...");
return new Promise((resolve, reject) => {
const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], {
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true
});
let scriptContent = "";
let curlErr = "";
curlChild.stdout.on("data", (d) => { scriptContent += d.toString(); });
curlChild.stderr.on("data", (d) => { curlErr += d.toString(); });
curlChild.on("exit", (code) => {
if (code !== 0) return reject(new Error(`Failed to download install script: ${curlErr}`));
log("Running install script...");
// Persist script to temp file → exec by path (NOT via stdin) → sh never reads attacker-controlled stdin
const tmpScript = path.join(os.tmpdir(), `tailscale-install-${crypto.randomBytes(8).toString("hex")}.sh`);
try {
fs.writeFileSync(tmpScript, scriptContent, { mode: 0o700 });View on GitHub (pinned to 90b52e06ff)
Solutions
- Re-enter the sudo password ensuring it is a single line with no newline characters (trim trailing whitespace/newlines).
- Verify the value passed is actually a string (typeof pw === 'string').
- If automation supplies the password, sanitize: password.replace(/\n/g, '') or pick a newline-free password.
- On passwordless-sudo systems, run the install manually (`curl -fsSL https://tailscale.com/install.sh | sh`) to avoid the sudo-password path entirely.
Example fix
// before
await installTailscale(passwordFromFile); // may contain "\n"
// after
const pw = String(passwordFromFile ?? "").replace(/[\r\n]+$/, "");
if (!pw || pw.includes("\n")) throw new Error("sudo password must be a single line");
await installTailscale(pw); Defensive patterns
Strategy: validation
Validate before calling
function assertSudoPassword(pw) {
if (typeof pw !== "string" || pw.length === 0 || pw.includes("\n")) {
throw new Error("sudo password must be a non-empty single-line string");
}
}
assertSudoPassword(password); Type guard
const isValidSudoPassword = (pw) => typeof pw === "string" && pw.length > 0 && !pw.includes("\n"); Try / catch
try {
await installTailscale(pw);
} catch (e) {
if (e.message === "Invalid sudo password") {
console.error("Password must be a single line without newlines — re-prompt user.");
return repromptForPassword();
}
throw e;
} Prevention
- Trim trailing newlines when reading passwords from files/configs.
- Never store passwords in multi-line formats; validate at input time.
- Prompt interactively instead of piping from files where possible.
- Prefer passwordless sudo or run the install script manually in automation.
When it happens
Trigger: Calling installTailscale() on Linux with a sudo password that is not a string (undefined/null/number) or that contains an embedded newline character. This happens when the password was read from a config/multi-line paste or a field containing trailing newline(s) beyond a single terminating one handled by the spawn wiring.
Common situations: Passwords pasted from a file/editor that kept a trailing blank line, passwords stored in JSON/YAML where '\n' was literally embedded, passing an empty/undefined variable as the password, or programmatically supplying credentials without trimming.
Related errors
- Installation finished but tailscale.exe not found
- Tailscale not installed
- Certificate install failed: ${error.message}
- AWS EventStream frame bounds are invalid
- Xquik queryType must be Latest or Top
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/daf253afa5c730a0.
Report an issue: GitHub.