Merge pull request #943 from dadtronics/security/harden-downloads-and-deletes

feat: harden downloads, deletes, debug tracing, and sudo handling
This commit is contained in:
Roy
2026-07-02 19:27:39 -07:00
committed by GitHub
2 changed files with 170 additions and 28 deletions
+9 -11
View File
@@ -4,14 +4,12 @@
logged_in_user=$(logname 2>/dev/null || whoami)
logged_in_home=$(eval echo "~${logged_in_user}")
# Function to prompt for sudo password
# Function to prompt for sudo using sudo's normal credential handling, instead of
# capturing the password with zenity and piping it into `sudo -S` (which exposes it
# via the environment/argv and process listings).
prompt_for_sudo() {
password=$(zenity --password --title="Authentication Required" --text="Please enter your password to proceed with installation/update.")
# Validate password
echo "$password" | sudo -S -v >/dev/null 2>&1
if [ $? -ne 0 ]; then
zenity --error --text="Incorrect password or sudo failed. Exiting."
if ! sudo -v; then
zenity --error --text="Sudo authentication failed. Exiting."
exit 1
fi
}
@@ -117,14 +115,14 @@ else
if $NSL_PLUGIN_EXISTS; then
show_message "NSL Plugin detected. Deleting and updating..."
echo "$password" | sudo -S rm -rf "$LOCAL_DIR"
sudo rm -rf "$LOCAL_DIR"
fi
show_message "Creating base directory and setting permissions..."
echo "$password" | sudo -S mkdir -p "$LOCAL_DIR"
echo "$password" | sudo -S chmod -R u+rw "$LOCAL_DIR"
echo "$password" | sudo -S chown -R "$logged_in_user:$logged_in_user" "$LOCAL_DIR"
sudo mkdir -p "$LOCAL_DIR"
sudo chmod -R u+rw "$LOCAL_DIR"
sudo chown -R "$logged_in_user:$logged_in_user" "$LOCAL_DIR"
curl -L "$REPO_URL" -o /tmp/NonSteamLaunchersDecky.zip
unzip -o /tmp/NonSteamLaunchersDecky.zip -d /tmp/
+161 -17
View File
@@ -6,7 +6,10 @@ flock -n 200 || {
exit 0
}
set -x # activate debugging (execution shown)
if [[ "${NSL_DEBUG:-0}" == "1" ]]; then
set -x # activate debugging only when requested; otherwise `set -x`
# echoes every expanded value (paths, tokens) into the log
fi
set -o pipefail # capture error from pipes
# ENVIRONMENT VARIABLES
@@ -37,6 +40,87 @@ logged_in_home=$(eval echo "~${logged_in_user}")
download_dir="${logged_in_home}/Downloads/NonSteamLaunchersInstallation"
log_file="${logged_in_home}/Downloads/NonSteamLaunchers-install.log"
# --- Security helpers ---------------------------------------------------------
# Set NSL_DRY_RUN=1 to preview destructive actions (deletes, Start Fresh) without
# performing them.
NSL_DRY_RUN="${NSL_DRY_RUN:-0}"
# Download over HTTPS only, with optional SHA-256 verification. Refuses non-HTTPS
# URLs and removes the file on checksum mismatch so a tampered/partial download is
# never executed or installed.
nsl_download() {
local url="$1"
local output="$2"
local expected_sha256="${3:-}"
if [[ "$url" != https://* ]]; then
echo "Refusing to download non-HTTPS URL: $url"
return 1
fi
if command -v curl >/dev/null 2>&1; then
curl --fail --location --silent --show-error --proto '=https' --tlsv1.2 --output "$output" "$url" || return 1
elif command -v wget >/dev/null 2>&1; then
wget --https-only -O "$output" "$url" || return 1
else
echo "Neither curl nor wget is installed."
return 1
fi
if [[ -n "$expected_sha256" ]]; then
local actual_sha256
actual_sha256=$(sha256sum "$output" | awk '{print $1}')
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
echo "Checksum mismatch for $output"
echo "Expected: $expected_sha256"
echo "Actual: $actual_sha256"
rm -f "$output"
return 1
fi
fi
}
# Delete only paths inside known NSL locations. Refuses empty/"/"/$HOME and any
# path outside the allowlist so a bad variable expansion can't wipe unrelated data.
# Honours NSL_DRY_RUN.
nsl_safe_rm() {
local path="$1"
if [[ -z "$path" || "$path" == "/" || "$path" == "$logged_in_home" ]]; then
echo "Refusing to delete unsafe path: ${path:-<empty>}"
return 1
fi
case "$path" in
"$download_dir"|"$download_dir"/*|\
"${logged_in_home}/.config/systemd/user/NSLGameScanner.py"|\
"${logged_in_home}/.config/systemd/user/descriptions.json"|\
"${logged_in_home}/.config/systemd/user/nslgamescanner.service"|\
"${logged_in_home}/.config/systemd/user/env_vars"|\
"${logged_in_home}/.local/share/applications/RemotePlayWhatever"|\
"${logged_in_home}/.local/share/applications/RemotePlayWhatever.desktop"|\
/tmp/NonSteamLaunchersDecky|/tmp/NonSteamLaunchersDecky.zip|/tmp/NonSteamLaunchersDecky-main|\
/tmp/hytale-launcher.flatpak)
;;
*)
echo "Refusing to delete path outside expected NSL locations: $path"
return 1
;;
esac
if [[ "$NSL_DRY_RUN" == "1" ]]; then
echo "DRY RUN: would delete $path"
return 0
fi
if [[ -L "$path" ]]; then
unlink "$path"
else
rm -rf "$path"
fi
}
# ------------------------------------------------------------------------------
#Cleaning
@@ -253,7 +337,7 @@ get_steam_user_info() {
{
set +x
steamid3=$(get_steam_user_info | tail -n1)
set -x
[[ "${NSL_DEBUG:-0}" == "1" ]] && set -x
} 2>/dev/null
# Get Proton compatibility tool name or fall back
@@ -468,7 +552,7 @@ fi
# Check if the NonSteamLaunchersInstallation subfolder exists in the Downloads folder
if [ -d "$download_dir" ]; then
# Delete the NonSteamLaunchersInstallation subfolder
rm -rf "$download_dir"
nsl_safe_rm "$download_dir"
echo "Deleted NonSteamLaunchersInstallation subfolder"
else
echo "NonSteamLaunchersInstallation subfolder does not exist"
@@ -1227,7 +1311,12 @@ echo "Options: $options"
# Define the StartFreshFunction
StartFreshFunction() {
sd_path=$(get_sd_path)
if [[ "$NSL_DRY_RUN" == "1" ]]; then
echo "DRY RUN: Start Fresh will report planned removals without deleting files or changing services."
sd_path="${NSL_DRY_RUN_SD_PATH:-/run/media/${USER:-deck}/DRY_RUN_SD_CARD}"
else
sd_path=$(get_sd_path)
fi
compatdata_dir="${logged_in_home}/.local/share/Steam/steamapps/compatdata"
other_dir="${logged_in_home}/.local/share/Steam/steamapps/shadercache"
@@ -1238,7 +1327,39 @@ StartFreshFunction() {
delete_path() {
local path="$1"
if [ -e "$path" ]; then
# Never delete empty / root / the home directory itself.
if [[ -z "$path" || "$path" == "/" || "$path" == "$logged_in_home" ]]; then
echo "Refusing to delete unsafe path: ${path:-<empty>}"
return 1
fi
# Only delete paths inside the known NSL locations this function operates on,
# so a bad variable expansion can't wipe unrelated data.
case "$path" in
"$compatdata_dir"/*|"$other_dir"/*|"$sd_path"/*|\
"${logged_in_home}/Downloads/NonSteamLaunchersInstallation"|\
"${logged_in_home}/.config/systemd/user/Modules"|\
"${logged_in_home}/.config/systemd/user/shortcuts"|\
"${logged_in_home}/.config/systemd/user/descriptions.json"|\
"${logged_in_home}/.config/systemd/user/NSLGameScanner.py"|\
"${logged_in_home}/.config/systemd/user/env_vars"|\
"${logged_in_home}/.config/systemd/user/nslgamescanner.service"|\
"${logged_in_home}/.local/share/applications/RemotePlayWhatever"|\
"${logged_in_home}/.local/share/applications/RemotePlayWhatever.desktop"|\
"${logged_in_home}/Downloads/NonSteamLaunchers-install.log")
;;
*)
echo "Refusing to delete path outside expected NSL locations: $path"
return 1
;;
esac
if [ -e "$path" ] || [ -L "$path" ]; then
if [[ "$NSL_DRY_RUN" == "1" ]]; then
echo "DRY RUN: would delete $path"
return 0
fi
if [ -L "$path" ]; then
target=$(readlink -f "$path")
rm -rf "$target"
@@ -1254,6 +1375,11 @@ StartFreshFunction() {
clean_envars_file() {
local envars_file="${logged_in_home}/.config/systemd/user/env_vars"
if [[ "$NSL_DRY_RUN" == "1" ]]; then
echo "DRY RUN: would clean ENVARS file at $envars_file"
return 0
fi
if [ -f "$envars_file" ]; then
echo "Cleaning ENVARS file..."
@@ -1276,6 +1402,11 @@ StartFreshFunction() {
run_nsl_scanner() {
local scanner_path="${logged_in_home}/.config/systemd/user/NSLGameScanner.py"
if [[ "$NSL_DRY_RUN" == "1" ]]; then
echo "DRY RUN: would run NSLGameScanner.py from $scanner_path"
return 0
fi
if [ -f "$scanner_path" ]; then
echo "Running NSLGameScanner.py with cleaned ENVARS..."
@@ -1332,6 +1463,13 @@ StartFreshFunction() {
delete_path "${logged_in_home}/.config/systemd/user/env_vars"
delete_path "${logged_in_home}/.config/systemd/user/nslgamescanner.service"
if [[ "$NSL_DRY_RUN" == "1" ]]; then
echo "DRY RUN: would unlink nslgamescanner.service symlink and reload/reset systemd user units"
show_message "DRY RUN complete — nothing was deleted."
exit 0
fi
unlink "${logged_in_home}/.config/systemd/user/default.target.wants/nslgamescanner.service" 2>/dev/null || true
systemctl --user daemon-reload
@@ -1361,7 +1499,13 @@ if [[ $options == "Start Fresh" ]] || [[ $selected_launchers == "Start Fresh" ]]
exit 0
fi
else
# Command line arguments were provided, so skip displaying the zenity window and directly perform any necessary actions to start fresh by calling the StartFreshFunction
# Command line arguments were provided, so skip the zenity window. Require an
# explicit opt-in so an accidental/automated "Start Fresh" arg can't silently
# wipe everything without confirmation.
if [[ "${NSL_CONFIRM_START_FRESH:-0}" != "1" ]]; then
echo "Refusing CLI Start Fresh without NSL_CONFIRM_START_FRESH=1"
exit 1
fi
StartFreshFunction
fi
fi
@@ -2056,8 +2200,8 @@ process_uninstall_options() {
fi
if [[ $uninstall_options == *"Uninstall RemotePlayWhatever"* ]]; then
rm -rf "${logged_in_home}/.local/share/applications/RemotePlayWhatever"
rm -rf "${logged_in_home}/.local/share/applications/RemotePlayWhatever.desktop"
nsl_safe_rm "${logged_in_home}/.local/share/applications/RemotePlayWhatever"
nsl_safe_rm "${logged_in_home}/.local/share/applications/RemotePlayWhatever.desktop"
zenity --info --text="RemotePlayWhatever has been uninstalled." --width=200 --height=150 &
sleep 3
@@ -2193,7 +2337,7 @@ if [ $# -gt 0 ]; then
# Check the flag after the loop
if $uninstalled_any_launcher; then
echo "Uninstallation completed successfully."
rm -rf "$download_dir"
nsl_safe_rm "$download_dir"
exit 0
fi
else
@@ -2254,7 +2398,7 @@ else
process_uninstall_options "Uninstall $launcher"
done
echo "Uninstallation completed successfully."
rm -rf "$download_dir"
nsl_safe_rm "$download_dir"
exit 0
fi
fi
@@ -2384,11 +2528,11 @@ function stop_service {
systemctl --user stop nslgamescanner.service
# Delete the NSLGameScanner.py
rm -rf ${logged_in_home}/.config/systemd/user/NSLGameScanner.py
rm -rf ${logged_in_home}/.config/systemd/user/descriptions.json
nsl_safe_rm "${logged_in_home}/.config/systemd/user/NSLGameScanner.py"
nsl_safe_rm "${logged_in_home}/.config/systemd/user/descriptions.json"
# Delete the service file
rm -rf ${logged_in_home}/.config/systemd/user/nslgamescanner.service
nsl_safe_rm "${logged_in_home}/.config/systemd/user/nslgamescanner.service"
# Remove the symlink
unlink ${logged_in_home}/.config/systemd/user/default.target.wants/nslgamescanner.service
@@ -2467,7 +2611,7 @@ if [[ " ${args[@]} " =~ " 🔍 " ]] || [[ $options == "🔍" ]]; then
# If command line arguments were provided, exit the script
if [ ${#args[@]} -ne 0 ]; then
rm -rf ${logged_in_home}/.config/systemd/user/env_vars
nsl_safe_rm "${logged_in_home}/.config/systemd/user/env_vars"
exit 0
fi
@@ -2742,7 +2886,7 @@ function install_eaapp {
mkdir -p "$eaapp_download_dir"
# Download the file
wget "$eaapp_url" -O "${eaapp_download_dir}${eaapp_file_name}"
nsl_download "$eaapp_url" "${eaapp_download_dir}${eaapp_file_name}" || exit 1
}
@@ -3127,7 +3271,7 @@ function install_launcher {
# Download file
if [ ! -f "$file_name" ]; then
echo "Downloading ${file_name}"
curl -L "$file_url" -o "$file_name"
nsl_download "$file_url" "$file_name" || exit 1
fi
# Execute the pre-installation command, if provided
@@ -4418,7 +4562,7 @@ echo "export chrome_startdir=$chrome_startdir" >> ${logged_in_home}/.config/syst
# TODO: might be better to relocate temp files to `/tmp` or even use `mktemp -d` since `rm -rf` is potentially dangerous without the `-i` flag
# Delete NonSteamLaunchersInstallation subfolder in Downloads folder
rm -rf "$download_dir"
nsl_safe_rm "$download_dir"