6448ea4f22
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/<user>, //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) <noreply@anthropic.com>
119 lines
4.5 KiB
Bash
Executable File
119 lines
4.5 KiB
Bash
Executable File
#!/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 <source> <mountpoint> <credfile> <vers> <opts> <fmode> <dmode>
|
|
# synology-nas-helper unmount <mountpoint>
|
|
#
|
|
# Guarantees enforced below:
|
|
# * mountpoint must resolve to a path under the CALLING user's $HOME or
|
|
# /run/media/<user> (rejects .. traversal via realpath -m).
|
|
# * source must look like //host/share.
|
|
# * credfile must live under /run/user/<caller-uid>/, 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
|