Found a problem with the no screen reader path that could have a user type password when unnecessary and potentially leak the password via speech if key echo is enabled.

This commit is contained in:
Storm Dragon
2026-07-21 22:21:02 -04:00
parent 46de80a7af
commit 2188921787
+132 -18
View File
@@ -8,9 +8,9 @@ import string
import subprocess
import sys
import tempfile
import threading
import time
import pwd
import threading
stormuxAdmin = ("storm",)
@@ -24,16 +24,41 @@ pingIntervalSeconds = 180
pingCount = 5
maxWormholeFailures = 3
sudoRefreshIntervalSeconds = 60
sudoKeepaliveThread = None
sudoKeepaliveStop = threading.Event()
sudoSessionPrepared = False
sudoAuthenticationError = ""
def speak_message(message):
def speak_message(
message,
fallbackFile=None,
wait=False,
applicationName=None,
connectionName=None,
):
command = ["spd-say"]
if applicationName:
command += ["-N", applicationName]
if connectionName:
command += ["-n", connectionName]
if wait:
command.append("-w")
command.append(message)
try:
subprocess.run(["spd-say", message], check=False)
result = subprocess.run(
command,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=wait,
)
except FileNotFoundError:
print(message, flush=True)
print(message, file=fallbackFile or sys.stdout, flush=True)
return
if result.returncode != 0:
print(message, file=fallbackFile or sys.stdout, flush=True)
def say_or_print(message, useSpeech):
if useSpeech:
@@ -53,16 +78,76 @@ def run_command(command, inputText=None, check=False, env=None):
)
def run_terminal_sudo_authentication():
speak_message(
"Your sudo password will be requested next. Do not type your password yet. "
"If your microphone can carry computer speech into the support meeting, mute it "
"before continuing and unmute it afterward. Press Enter to continue. After you "
"press Enter, wait one second, then type your password and press Enter. If you "
"hear any part of your password spoken, stop immediately, do not finish it, and "
"tell the support assistant.",
wait=True,
applicationName="sas",
connectionName=f"sudo-password-{os.getpid()}",
)
input()
return run_command(["sudo", "-v"])
def authenticate_sudo(useSpeech):
global sudoAuthenticationError
sudoAuthenticationError = ""
command = ["sudo"]
if useSpeech:
result = run_terminal_sudo_authentication()
else:
result = run_command(command + ["-v"])
if result.returncode != 0:
sudoAuthenticationError = concise_command_error(result)
return False
start_sudo_keepalive()
return True
def prepare_sudo_session(useSpeech):
global sudoAuthenticationError, sudoSessionPrepared
if os.geteuid() == 0:
sudoSessionPrepared = True
return True
invalidateResult = run_command(["sudo", "-k"])
if invalidateResult.returncode != 0:
sudoAuthenticationError = concise_command_error(invalidateResult)
return False
probeEnv = os.environ.copy()
probeEnv["LC_ALL"] = "C"
probeEnv["LANG"] = "C"
probeResult = run_command(["sudo", "-n", "-v"], env=probeEnv)
if probeResult.returncode == 0:
start_sudo_keepalive()
sudoSessionPrepared = True
return True
probeError = concise_command_error(probeResult)
if "password is required" not in probeError.lower():
sudoAuthenticationError = probeError
return False
if not authenticate_sudo(useSpeech=useSpeech):
return False
sudoSessionPrepared = True
return True
def ensure_sudo(useSpeech):
if os.geteuid() == 0:
return True
if useSpeech:
speak_message("Sudo password required. Please enter your password now.")
result = run_command(["sudo", "-v"])
if result.returncode == 0:
start_sudo_keepalive()
return True
return False
if sudoSessionPrepared:
return run_command(["sudo", "-n", "-v"]).returncode == 0
return authenticate_sudo(useSpeech)
def sudo_command_prefix():
if sudoSessionPrepared:
return ["sudo", "-n"]
return ["sudo"]
def start_sudo_keepalive():
@@ -71,7 +156,7 @@ def start_sudo_keepalive():
return
def keepalive_loop():
while not sudoKeepaliveStop.wait(240):
while not sudoKeepaliveStop.wait(sudoRefreshIntervalSeconds):
run_command(["sudo", "-n", "-v"])
sudoKeepaliveThread = threading.Thread(target=keepalive_loop, daemon=True)
@@ -84,7 +169,7 @@ def run_privileged(command, useSpeech, inputText=None, check=True):
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo"] + command
fullCommand = sudo_command_prefix() + command
return run_command(fullCommand, inputText=inputText, check=check)
@@ -94,7 +179,7 @@ def run_as_user(userName, command, useSpeech, check=True):
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo", "-u", userName, "-H"] + command
fullCommand = sudo_command_prefix() + ["-u", userName, "-H"] + command
return run_command(fullCommand, check=check)
@@ -102,6 +187,15 @@ def command_error_text(result):
return (result.stderr or result.stdout or "").strip()
def concise_command_error(result):
errorText = " ".join(command_error_text(result).split())
if not errorText:
return "sudo returned no error details"
if len(errorText) > 400:
return errorText[:400] + "..."
return errorText
def service_is_active(serviceName):
result = run_command(["systemctl", "is-active", "--quiet", serviceName])
return result.returncode == 0
@@ -373,6 +467,15 @@ def main():
answer = input().strip().lower()
useSpeech = answer in ("n", "no")
if useSpeech and not prepare_sudo_session(useSpeech=True):
failureMessage = (
f"Sudo authentication failed. {sudoAuthenticationError}. "
"The support session was not started."
)
say_or_print(failureMessage, True)
print(failureMessage, flush=True)
return 1
shouldRemoveUser = False
cleanupDone = False
tempDir = tempfile.mkdtemp(prefix="sas-ii-")
@@ -425,7 +528,10 @@ def main():
useSpeech,
)
except Exception:
pass
say_or_print(
"Cleanup warning: failed to remove sas user. Please remove it manually.",
useSpeech,
)
sudoKeepaliveStop.set()
if sudoKeepaliveThread:
@@ -615,7 +721,7 @@ def main():
privateKeyPath,
f"{sasUser}@{remoteHost}",
]
sshCommand = ["sudo", "-u", sasUser, "-H"] + sshCommand
sshCommand = sudo_command_prefix() + ["-u", sasUser, "-H"] + sshCommand
sshProcess = subprocess.Popen(sshCommand)
sshProcess.wait()
say_or_print("The support connection ended. Cleaning up.", useSpeech)
@@ -649,5 +755,13 @@ def remove_tree(path):
pass
def command_line_main(arguments=None):
arguments = sys.argv[1:] if arguments is None else arguments
if arguments:
print("Usage: sas", file=sys.stderr)
return 2
return main()
if __name__ == "__main__":
sys.exit(main())
sys.exit(command_line_main())