From 6448ea4f22d2d1d42cb1987b35fb9b0633e64127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Wed, 15 Jul 2026 14:28:49 +0200 Subject: [PATCH] Add privileged mount helper + polkit policy POSIX sh helper (pkexec target) that mounts/unmounts CIFS shares with strict validation of the unprivileged caller's arguments: mountpoint confined to $HOME or /run/media/, //host/share source, credfile under the caller's runtime dir at mode 0600, whitelisted SMB version/options/modes. Polkit action authorizes it with auth_admin_keep. Co-Authored-By: Claude Opus 4.8 (1M context) --- helper/sk.houmeres.synologynas.policy | 20 +++++ helper/synology-nas-helper | 118 ++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 helper/sk.houmeres.synologynas.policy create mode 100755 helper/synology-nas-helper diff --git a/helper/sk.houmeres.synologynas.policy b/helper/sk.houmeres.synologynas.policy new file mode 100644 index 0000000..fe612c8 --- /dev/null +++ b/helper/sk.houmeres.synologynas.policy @@ -0,0 +1,20 @@ + + + + Synology NAS plasmoid + https://git.houmeres.sk/Houmeres/Plasma-Addon-Synology-NAS + folder-network + + + Mount or unmount a network share + Authentication is required to mount or unmount a network share + + no + no + auth_admin_keep + + /usr/lib/synology-nas/helper + true + + diff --git a/helper/synology-nas-helper b/helper/synology-nas-helper new file mode 100755 index 0000000..53a0d64 --- /dev/null +++ b/helper/synology-nas-helper @@ -0,0 +1,118 @@ +#!/bin/sh +# +# synology-nas-helper — privileged mount/unmount helper for the Synology NAS +# plasmoid. It is invoked as root via pkexec (see the accompanying +# sk.houmeres.synologynas.policy). The CALLER IS UNPRIVILEGED, so every argument +# is treated as hostile and strictly validated before any mount happens. +# +# Usage (all arguments positional): +# synology-nas-helper mount +# synology-nas-helper unmount +# +# Guarantees enforced below: +# * mountpoint must resolve to a path under the CALLING user's $HOME or +# /run/media/ (rejects .. traversal via realpath -m). +# * source must look like //host/share. +# * credfile must live under /run/user//, be a regular file, +# owned by the caller, mode 0600. +# * vers, permission modes and extra options are charset/whitelist checked. +# +set -eu + +die() { echo "synology-nas-helper: $1" >&2; exit 1; } + +# --- identify the calling (unprivileged) user --------------------------------- +[ -n "${PKEXEC_UID:-}" ] || die "must be run through pkexec (PKEXEC_UID unset)" +case "$PKEXEC_UID" in + ''|*[!0-9]*) die "invalid PKEXEC_UID" ;; +esac + +pwline=$(getent passwd "$PKEXEC_UID") || die "unknown caller uid $PKEXEC_UID" +CALLER_USER=$(printf '%s\n' "$pwline" | cut -d: -f1) +CALLER_GID=$(printf '%s\n' "$pwline" | cut -d: -f4) +CALLER_HOME=$(printf '%s\n' "$pwline" | cut -d: -f6) +[ -n "$CALLER_HOME" ] || die "caller has no home directory" + +# --- shared validators -------------------------------------------------------- +validate_mountpoint() { + # Canonicalise (resolves .. lexically, no need for the path to exist yet). + canon=$(realpath -m -- "$1") || die "cannot canonicalise mountpoint" + case "$canon" in + "$CALLER_HOME"/*) : ;; + "/run/media/$CALLER_USER"/*) : ;; + *) die "mountpoint must be under \$HOME or /run/media/$CALLER_USER" ;; + esac + printf '%s' "$canon" +} + +# --- subcommands -------------------------------------------------------------- +do_mount() { + [ $# -eq 7 ] || die "mount: expected 7 arguments" + source=$1; mountpoint=$2; credfile=$3; vers=$4; extra=$5; fmode=$6; dmode=$7 + + case "$source" in + //?*/?*) : ;; + *) die "source must be //host/share" ;; + esac + + mp=$(validate_mountpoint "$mountpoint") + + # Credentials file: under the caller's runtime dir, their file, mode 0600. + cf=$(realpath -m -- "$credfile") || die "cannot canonicalise credfile" + case "$cf" in + "/run/user/$PKEXEC_UID"/*) : ;; + *) die "credfile must be under /run/user/$PKEXEC_UID" ;; + esac + [ -f "$cf" ] || die "credfile not found" + [ "$(stat -c %u "$cf")" = "$PKEXEC_UID" ] || die "credfile not owned by caller" + [ "$(stat -c %a "$cf")" = "600" ] || die "credfile must be mode 0600" + + case "$vers" in + default|1.0|2.0|2.1|3.0|3.02|3.1.1) : ;; + *) die "unsupported SMB version: $vers" ;; + esac + case "$fmode" in 0[0-7][0-7][0-7]) : ;; *) die "bad file mode" ;; esac + case "$dmode" in 0[0-7][0-7][0-7]) : ;; *) die "bad dir mode" ;; esac + case "$extra" in + '') : ;; + *[!A-Za-z0-9=,_.:-]*) die "extra mount options contain illegal characters" ;; + esac + + if findmnt -rn -o TARGET --mountpoint "$mp" >/dev/null 2>&1; then + die "already mounted: $mp" + fi + + # Create the mount point and hand the freshly-created dirs to the caller. + mkdir -p "$mp" || die "cannot create mount point" + chown "$PKEXEC_UID:$CALLER_GID" "$mp" 2>/dev/null || true + p=$(dirname "$mp") + while [ "$p" != "$CALLER_HOME" ] && [ "$p" != "/" ] && [ "$p" != "/run/media" ]; do + chown "$PKEXEC_UID:$CALLER_GID" "$p" 2>/dev/null || true + p=$(dirname "$p") + done + + opts="credentials=$cf,uid=$PKEXEC_UID,gid=$CALLER_GID,forceuid,forcegid,file_mode=$fmode,dir_mode=$dmode,vers=$vers" + [ -n "$extra" ] && opts="$opts,$extra" + + exec mount.cifs "$source" "$mp" -o "$opts" +} + +do_unmount() { + [ $# -eq 1 ] || die "unmount: expected 1 argument" + mp=$(validate_mountpoint "$1") + findmnt -rn -t cifs -o TARGET --mountpoint "$mp" >/dev/null 2>&1 \ + || die "not a cifs mount: $mp" + umount "$mp" || die "umount failed" + # Tidy up empty leaf + host dirs (never recursive, never touches files). + rmdir "$mp" 2>/dev/null || true + rmdir "$(dirname "$mp")" 2>/dev/null || true +} + +[ $# -ge 1 ] || die "no subcommand" +cmd=$1 +shift +case "$cmd" in + mount) do_mount "$@" ;; + unmount) do_unmount "$@" ;; + *) die "unknown subcommand: $cmd" ;; +esac