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

  1. Re-enter the sudo password ensuring it is a single line with no newline characters (trim trailing whitespace/newlines).
  2. Verify the value passed is actually a string (typeof pw === 'string').
  3. If automation supplies the password, sanitize: password.replace(/\n/g, '') or pick a newline-free password.
  4. 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

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


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/daf253afa5c730a0. Report an issue: GitHub.