wren-linux / bin / firefox
#!/bin/bash
# firefox [URL...]: written by Wren Linux extras/firefox-sandbox.sh
# Opens Firefox as a different, locked-down user (firefox), as a normal window
# on your sway. Your logins and history are kept (in that user's own home), but
# a hacked page reaches only that user: not your files, ssh keys or passwords.
# - Clipboard: shared with your programs while the Firefox window has focus.
# - Downloads: ~/Downloads is shared both ways. Treat what lands there as
# untrusted, and do not run it without looking. A file there can be opened
# in it too: firefox file://$HOME/Downloads/page.html
# - Screen: it gets a restricted Wayland socket (bin/wayland-restrict), not your
# real one, so it cannot read the screen, type for you or lock the session.
# - Sound: its own audio server whose only output is one pipe, which a player of
# yours plays (as in incognito). It cannot hear your microphone.
# - Network: the firewall keeps it off your router, printers and this computer's
# own services (data/firefox-firewall.nft). To use one for a while:
# sudo systemctl stop wren-firefox-firewall
# - Links from other programs open in the Firefox that is already running.
# firefox-unsafe runs Firefox as you, with all your files and no such rules
# (extras/firefox-sandbox.sh moves Debian's firefox-esr there; firefox-esr opens this).
# - A profile made with new-profile.sh has a user of its own (fx-NAME), its own
# downloads folder (~/Downloads/NAME) and runs next to the others:
# firefox --profile NAME (its own command, NAME or firefox-NAME, does this)
# firefox [--no-audio] [--profile NAME] [URL...]
# - Profiles, each a Firefox of its own for one site (extras/firefox-profiles):
# firefox --profiles list them
# firefox --new-profile [NAME URL [...]] make one (asks, without NAME and URL;
# --fast-js --hide-toolbars --no-sound --keep-cookies --ublock-standard)
# firefox --remove-profile NAME remove one (asks before deleting its logins)
# shellcheck disable=SC2154 # rt, share, sock, audio_arg: set by the wren-sandbox functions
set -euo pipefail
die() { printf 'firefox: %s\n' "$*" >&2; exit 1; }
PROFILES=${WREN_FX_PROFILE_DIR:-/etc/wren/firefox-profiles}
# The profiles: their settings files, which root writes and anyone may read
list_profiles() {
local f name url cmd extra
for f in "$PROFILES"/*.conf; do
[ -f "$f" ] || continue
name=${f##*/}; name=${name%.conf}
[[ $name =~ ^[a-z][a-z0-9-]{0,27}$ ]] || continue
url=$(sed -n 's/^url=//p' "$f" | head -n1)
cmd="(no command)"
for c in "$HOME/.local/bin/$name" "$HOME/.local/bin/firefox-$name"; do
if grep -qs 'Written by Wren Linux extras/firefox-profiles/new-profile.sh' "$c"; then cmd=${c##*/}; fi
done
extra=""
[ "$(sed -n 's/^fast_js=//p' "$f")" != yes ] || extra+=", fast JavaScript"
[ "$(sed -n 's/^sound=//p' "$f")" != no ] || extra+=", no sound"
[ "$(sed -n 's/^cookies=//p' "$f")" != keep ] || extra+=", cookies kept"
[ "$(sed -n 's/^toolbars=//p' "$f")" != hidden ] || extra+=", page only"
printf '%-16s %-20s %s%s\n' "$name" "$cmd" "$url" "$extra"
done
}
# new-profile.sh, from where Wren Linux is (wrenctl --dir)
new_profile() {
local dir
dir=$(wrenctl --dir 2>/dev/null) || die "wrenctl isn't installed, so new-profile.sh can't be found."
[ -f "$dir/extras/firefox-profiles/new-profile.sh" ] || die "$dir has no extras/firefox-profiles/new-profile.sh."
exec bash "$dir/extras/firefox-profiles/new-profile.sh" "$@"
}
case ${1:-} in
--profiles)
[ $# -eq 1 ] || die "usage: firefox --profiles"
out=$(list_profiles)
if [ -z "$out" ]; then
echo "No profiles yet. To make one: firefox --new-profile bank https://mybank.example"
else
printf '%-16s %-20s %s\n' PROFILE COMMAND ADDRESS
printf '%s\n' "$out"
fi
exit 0 ;;
--new-profile) shift; new_profile "$@" ;;
--remove-profile)
[ $# -eq 2 ] || die "usage: firefox --remove-profile NAME (firefox --profiles lists them)"
new_profile --remove "$2" ;;
esac
audio=true
name=
while [ $# -gt 0 ]; do
case $1 in
--no-audio) audio=false; shift ;;
--profile)
name=${2:-}
[[ $name =~ ^[a-z][a-z0-9-]{0,27}$ ]] || die "a profile name is lowercase letters, digits and dashes, e.g. bank."
shift 2 ;;
*) break ;;
esac
done
# user, lock, socket and sound pipe names: "firefox" alone, or one set per profile
if [ -z "$name" ]; then user=firefox tag=; else user=fx-$name tag=-$name; fi
# sound off when that was the choice: at the install for the main Firefox
# (/etc/wren/firefox.conf), at new-profile.sh for a profile (its own file)
if [ -z "$name" ]; then choices=/etc/wren/firefox.conf; else choices=$PROFILES/$name.conf; fi
[ "$(sed -n 's/^sound=//p' "$choices" 2>/dev/null | head -1)" != no ] || audio=false
WS=${WREN_SANDBOX_LIB:-/usr/local/libexec/wren-sandbox}
# shellcheck source=wren-sandbox
. "$WS" || die "$WS is missing. Run: wrenctl install firefox-sandbox"
ws_session
# A web address, or a file in this Firefox's own downloads folder (file://,
# the real path inside it: no .., no link out of it), which it can read anyway
url_ok() {
[[ $1 =~ ^https?://[^[:space:]]+$ ]] && return 0
[[ $1 =~ ^file://(/[^[:space:]%]+)$ ]] || return 1
local p=${BASH_REMATCH[1]} real top
case /$p/ in */../* | */./*) return 1 ;; esac
real=$(realpath -e -- "$p" 2>/dev/null) && top=$(realpath -e -- "$HOME/Downloads${name:+/$name}" 2>/dev/null) || return 1
[[ $real == "$top"/* ]]
}
for a in "$@"; do
url_ok "$a" || die "only http:// and https:// addresses, and files in $HOME/Downloads${name:+/$name}, can be opened."
done
SESSION=/usr/local/libexec/firefox-session
# The one command your user may run as that user (see /etc/sudoers.d)
ws_allowed "$SESSION" "wrenctl install firefox-sandbox${name:+ (or new-profile.sh for the profile $name)} (firefox-unsafe runs Firefox as you.)"
# Already open? Then the link (or a new window) goes to that Firefox.
if ! ws_lock "$rt/firefox$tag.lock"; then
exec 9>&-
exec sudo -n -u "$user" "$SESSION" open "$@"
fi
ws_tools "wrenctl install firefox-sandbox"
# (firefox-session accepts only /home/NAME/Downloads, or .../Downloads/PROFILE)
downloads=$HOME/Downloads${name:+/$name}
# What this Firefox is handed (the restricted socket, the sound pipe, the New Tab picture) is in a
# new folder of its own (ws_share): firefox-session accepts only such a folder.
ws_share "firefox$tag"
# The theme (bin/theme, light or dark): the sandbox gets just the word
theme=$(head -c 16 "${XDG_CONFIG_HOME:-$HOME/.config}/wren/theme" 2>/dev/null || true)
case $theme in light | dark) ;; *) theme=dark ;; esac
# Your New Tab picture, for the main Firefox: the newest JPEG or PNG in
# ~/Pictures/wren/firefox. The sandbox never sees that folder: a checked
# copy goes into its private folder, readable by firefox alone, and goes
# again when Firefox closes. (firefox-session checks it once more.)
picture=
picture_arg=-
if [ -z "$name" ]; then
best=
for p in "$HOME/Pictures/wren/firefox"/*; do
[ -f "$p" ] && [ -r "$p" ] || continue
case ${p,,} in *.jpg | *.jpeg | *.png) ;; *) continue ;; esac
[ "$(stat -L -c %s -- "$p")" -le $((10 * 1024 * 1024)) ] || continue
case $(head -c 8 -- "$p" | od -An -tx1 | tr -d ' \n') in 89504e470d0a1a0a | ffd8ff*) ;; *) continue ;; esac
if [ -z "$best" ] || [ "$p" -nt "$best" ]; then best=$p; fi
done
if [ -n "$best" ]; then
ext=jpg
case ${best,,} in *.png) ext=png ;; esac
picture=$share/firefox-newtab.$ext
(umask 077; cp -- "$best" "$picture")
picture_arg=${picture##*/}
fi
fi
# firefox may only reach the restricted Wayland socket, the sound pipe and the
# downloads folder, and only while it runs.
# shellcheck disable=SC2317,SC2329 # runs from the EXIT trap
ws_extra_cleanup() {
setfacl -x "u:$user" "$HOME" "$HOME/Downloads" 2>/dev/null || true
}
ws_traps
ws_wayland "firefox$tag-wayland" "firefox$tag"
# To reach what it is handed, and to pass through your home folder to ~/Downloads
# (traverse only: it cannot list or read anything there)
mkdir -p "$downloads"
# The sandboxed Firefox may write in ~/Downloads, so a profile's folder there
# could have been swapped for a link (to ~/.ssh, say): then the rights below
# would go to the folder it points at. Only a real folder of yours, where it
# should be (and setfacl -P never follows a link either).
[ ! -L "$downloads" ] && [ -d "$downloads" ] && [ -O "$downloads" ] &&
[ "$(cd -P -- "$downloads" && pwd)" = "$(cd -P -- "$HOME/Downloads" && pwd)${name:+/$name}" ] ||
die "$downloads isn't a folder of yours (a link, or someone else's): move it away, and start it again."
setfacl -m "u:$user:--x" "$HOME" "$HOME/Downloads"
if [ -n "$picture" ]; then setfacl -m "u:$user:r--" "$picture"; fi
# Read, write and enter the downloads folder, for both of you, also for files made later
setfacl -P -m "u:$user:rwx" "$downloads"
setfacl -P -d -m "u:$user:rwx,u:$(id -un):rwx" "$downloads"
# A profile's folder is made inside ~/Downloads, whose default rights name the
# main sandboxed Firefox (firefox), so the folder and what lands in it inherit
# them: taken off again, so only this profile's user and you can read it
if [ -n "$name" ]; then
setfacl -P -R -x "u:firefox,d:u:firefox" "$downloads" 2>/dev/null || true
fi
ws_audio "$audio" "firefox$tag-audio.fifo"
rc=0
sudo -n -u "$user" "$SESSION" start "$share" "${sock##*/}" "$audio_arg" "$downloads" "$theme" "$picture_arg" "$@" || rc=$?
exit "$rc"