Created
May 27, 2025 09:58
-
-
Save PhrozenByte/0d7dcfacf998db092c8e2ad3a4f7c7dc to your computer and use it in GitHub Desktop.
Safely source sh-based config files with `bwrap`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# Safely source sh-based config files with `bwrap` | |
# | |
# This function allows script developers to safely include sh-based config | |
# files, especially if the config file isn't owned by the running user. Most | |
# notably this function allows one to source config files owned by unprivileged | |
# users as root, without practically giving said unprivileged user root access. | |
# The file is sourced within an unprivileged bwrap container with read-only | |
# access to /usr and nothing else. The sh-based config file can thus do | |
# anything a sourced Bash file normally could (e.g. do complex calculations | |
# with all tools available on the host system to create the necessary config | |
# values), but can't do much harm on the host system. The only harmful action | |
# a tampered config file could still inflict is a Denial of Service attack, | |
# because the script file can still spawn arbitrary processes. | |
# | |
# Usage: | |
# source_config CONFIG_USER CONFIG_FILE [--env ENV_VARS]... CONFIG_VAR... | |
# | |
# Arguments and options | |
# CONFIG_USER Name of the user the config file shall be sourced with. | |
# CONFIG_FILE Path to the config file that shall be sourced. The file | |
# must be owned by CONFIG_USER, else `source_config` bails. | |
# --env ENV_VARS Space separated list of env variables to pass to `bwrap`. | |
# This option can be passed multiple times. | |
# CONFIG_VAR Name of a config variable to return. If suffixed by `[]`, | |
# the variable is returned as Bash array. This option can be | |
# passed multiple times. | |
# | |
# Copyright (C) 2025 Daniel Rudolf (<https://www.daniel-rudolf.de>) | |
# License: The MIT License <http://opensource.org/licenses/MIT> | |
# | |
# SPDX-License-Identifier: MIT | |
source_config() { | |
local __USER="$1" | |
local __FILE="$2" | |
shift 2 | |
local __UID="$(id -u "$__USER")" | |
local __GID="$(id -g "$__USER")" | |
if [ -z "$__UID" ] || [ -z "$__GID" ]; then | |
echo "Invalid argument for \`source_config\`: Invalid user ${__USER@Q}: No such user" >&2 | |
return 1 | |
fi | |
if [ ! -e "$__FILE" ]; then | |
echo "Invalid argument for \`source_config\`: Invalid config file ${__FILE@Q}: No such file or directory" >&2 | |
return 1 | |
elif [ ! -f "$__FILE" ]; then | |
echo "Invalid argument for \`source_config\`: Invalid config file ${__FILE@Q}: Not a file" >&2 | |
return 1 | |
elif [ ! -r "$__FILE" ]; then | |
echo "Invalid argument for \`source_config\`: Invalid config file ${__FILE@Q}: Permission denied" >&2 | |
return 1 | |
elif [ "$(stat -c '%u:%g' "$__FILE")" != "$__UID:$__GID" ]; then | |
echo "Invalid argument for \`source_config\`: Invalid config file ${__FILE@Q}: Invalid file ownership," \ | |
"expecting '$__UID:$__GID', got '$(stat -c '%u:%g' "$__FILE")'" >&2 | |
return 1 | |
fi | |
local -a __ENV=() | |
local -a __VARS=() | |
while [ $# -gt 0 ]; do | |
if [ "$1" == "--env" ]; then | |
[[ "${2:-}" =~ ^([a-zA-Z][a-zA-Z0-9_]*)(\[\])?(\ ([a-zA-Z][a-zA-Z0-9_]*)(\[\])?)*$ ]] \ | |
|| { echo "Invalid argument for \`source_config --env\`: ${2:-}" >&2; return 1; } | |
local __ARG | |
for __ARG in $2; do | |
[[ "$__ARG" =~ ^([a-zA-Z][a-zA-Z0-9_]*)(\[\])?$ ]] | |
__ENV+=( "$(declare -p "${BASH_REMATCH[1]}")" ) | |
done | |
shift 2 | |
else | |
[[ "$1" =~ ^([a-zA-Z][a-zA-Z0-9_]*)(\[\])?$ ]] \ | |
|| { echo "Invalid argument for \`source_config\`: $1" >&2; return 1; } | |
__VARS+=( "$1" ) | |
shift | |
fi | |
done | |
__read_vars() { | |
source /variables.sh | |
local __ARG __VALUE | |
for __ARG in "$@"; do | |
[[ "$__ARG" =~ ^([a-zA-Z][a-zA-Z0-9_]*)(\[\])?$ ]] || continue | |
if [ -v "${BASH_REMATCH[1]}" ]; then | |
local -n __REF="${BASH_REMATCH[1]}" | |
if [ -n "${BASH_REMATCH[2]}" ]; then | |
for __VALUE in "${__REF[@]}"; do | |
printf '%s=%s\0' "$__ARG" "$__VALUE" | |
done | |
elif [ -n "$__REF" ]; then | |
printf '%s=%s\0' "$__ARG" "$__REF" | |
fi | |
fi | |
done | |
} | |
__parse_vars() { | |
local -a __VARS | |
readarray -d '' -t __VARS | |
local -a __RAW | |
while [ $# -gt 0 ]; do | |
[[ "$1" =~ ^([a-zA-Z][a-zA-Z0-9_]*)(\[\])?$ ]] || continue | |
readarray -d '' -t __RAW < <(printf '%s\0' "${__VARS[@]}" \ | |
| sed -z -ne "s/^$(sed -e 's/[]\/$*.^[]/\\&/g' <<< "$1")=\(.*\)$/\1/p") | |
if [ "${#__RAW[@]}" -gt 0 ] || [ ! -v "${BASH_REMATCH[1]}" ]; then | |
if [ -n "${BASH_REMATCH[2]}" ]; then | |
local -n __REF="${BASH_REMATCH[1]}" | |
declare -g -a "${BASH_REMATCH[1]}" | |
__REF=( "${__RAW[@]}" ) | |
else | |
declare -g "${BASH_REMATCH[1]}"="${__RAW:-}" | |
fi | |
fi | |
shift | |
done | |
} | |
local -a __BWRAP_INVOC_OPTS=( | |
--unshare-all | |
--uid "$__UID" | |
--gid "$__GID" | |
) | |
local -a __BWRAP_FS_OPTS=( | |
--symlink usr/bin /bin | |
--dev /dev | |
--dir /etc | |
--symlink usr/lib /lib | |
--symlink usr/lib64 /lib64 | |
--proc /proc | |
--symlink usr/sbin /sbin | |
--dir /tmp | |
--ro-bind /usr /usr | |
--dir /var | |
--symlink ../tmp var/tmp | |
) | |
local -a __BWRAP_ENV_OPTS=( | |
--chdir / | |
--clearenv | |
) | |
local -a __BWRAP_COMMAND=( | |
"$(declare -f "__read_vars")" | |
"${__ENV[@]}" | |
'__read_vars "$@"' | |
) | |
__parse_vars "${__VARS[@]}" < <(bwrap \ | |
--die-with-parent \ | |
"${__BWRAP_INVOC_OPTS[@]}" \ | |
"${__BWRAP_FS_OPTS[@]}" \ | |
--file 11 /etc/passwd \ | |
--file 12 /etc/group \ | |
--file 13 /variables.sh \ | |
"${__BWRAP_ENV_OPTS[@]}" \ | |
/usr/bin/bash -c "$(printf '%s\n' "${__BWRAP_COMMAND[@]}")" _ "${__VARS[@]}" \ | |
11< <(getent passwd "$__UID" 65534) \ | |
12< <(getent group "$__GID" 65534) \ | |
13< <(cat "$__FILE")) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment