Compare commits

..

2 Commits

Author SHA1 Message Date
librelad
63e956ded4 Merge claude/2 2026-05-31 03:50:48 +01:00
librelad
f49455e38e fix(de-sudo): route all confirmed container-tree writes through the privileged path
Exhaustive audit (workflow: 19 finders + adversarial per-file verify; 85 raw ->
66 unique -> 39 confirmed) found 36 direct writes into the container-owned tree
that bypass runFileOp/runFileWrite/runCfgOp (manager => EACCES in rootless) plus
3 $?-masking sites. Fixes by area:

- apps: grafana + prometheus install hooks (sudo chmod -> runFileOp chmod);
  gluetun provider etag (tee -> runFileWrite).
- webui generators: task-create (10 sites: mkdir/chown/tee/jq|tee/sed|tee ->
  runFileOp/runFileWrite); app-icons (mkdir/cp/mv); config icon cp; system
  metrics + update throttle stamps (runAsManager touch -> runFileOp touch);
  setup-lock rm; updater history seed + cp.
- task health checker: 4 log writes (tee -a -> runFileWrite -a) + 3 find -delete
  (-> runFileOp find).
- config reconcile: backup cp -> runCfgOp; live cp -> runFileWrite < tmp for
  container-owned configs (the container user can't read a manager 0600 tmp).
- peer pull: tar extract into the container tree -> runFileOp tar.
- masking: ip_find_available + folder_group(x2) — split 'local VAR=$(cmd)' so $?
  reaches the following [[ $? ]] check.

15 files, all pass bash -n; fixed idioms confirmed gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 03:50:48 +01:00
15 changed files with 42 additions and 42 deletions

View File

@ -82,7 +82,7 @@ appWebuiRefresh_gluetun() {
if [ -s "$tmp" ]; then
runFileWrite "$output_file" < "$tmp"; rm -f "$tmp"
[[ -n "$new_etag" ]] && echo "$new_etag" | tee "$etag_file" >/dev/null
[[ -n "$new_etag" ]] && echo "$new_etag" | runFileWrite "$etag_file"
isSuccessful "Refreshed gluetun provider snapshot ($(jq '.providers | length' "$output_file") providers)."
else
rm -f "$tmp"

View File

@ -17,7 +17,7 @@ grafana_install_post_start()
local app_name="$1"
if [ -d "${containers_dir}grafana/grafana_storage" ]; then
local result
result=$(sudo chmod -R 777 "${containers_dir}grafana/grafana_storage")
result=$(runFileOp chmod -R 777 "${containers_dir}grafana/grafana_storage")
checkSuccess "Set permissions to grafana_storage folder."
fi
}

View File

@ -28,12 +28,12 @@ prometheus_install_post_start()
fi
if [ -d "${containers_dir}prometheus/prometheus" ]; then
local result
result=$(sudo chmod -R 777 "${containers_dir}prometheus/prometheus")
result=$(runFileOp chmod -R 777 "${containers_dir}prometheus/prometheus")
checkSuccess "Set permissions to prometheus folder."
fi
if [ -d "${containers_dir}prometheus/prom_data" ]; then
local result
result=$(sudo chmod -R 777 "${containers_dir}prometheus/prom_data")
result=$(runFileOp chmod -R 777 "${containers_dir}prometheus/prom_data")
checkSuccess "Set permissions to prom_data folder."
fi
}

View File

@ -156,13 +156,13 @@ updaterRecordHistory()
local app="$1" action="$2" from="$3" to="$4" result="$5"
local f="$containers_dir/libreportal/frontend/data/updater/generated/history.json"
command -v jq >/dev/null 2>&1 || return 0
[ -f "$f" ] || printf '{ "entries": [] }\n' > "$f"
[ -f "$f" ] || printf '{ "entries": [] }\n' | runFileWrite "$f"
local ts; ts="$(date -Iseconds 2>/dev/null || date)"
local tmp; tmp="$(mktemp)"
if jq --arg ts "$ts" --arg app "$app" --arg action "$action" --arg from "$from" --arg to "$to" --arg result "$result" \
'.entries = ([{ts:$ts, app:$app, action:$action, from:$from, to:$to, result:$result}] + (.entries // []))[0:200]' \
"$f" > "$tmp" 2>/dev/null; then
runFileOp cp "$tmp" "$f" 2>/dev/null || cp "$tmp" "$f"
runFileWrite "$f" < "$tmp"
fi
rm -f "$tmp"
}

View File

@ -86,8 +86,8 @@ reconcileConfigFile()
# Replace only when the result is sane (non-empty, has keys) and differs.
if [[ -s "$tmp" ]] && grep -q '^CFG_' "$tmp" && ! runInstallOp cmp -s "$tmp" "$live"; then
runInstallOp cp -a "$live" "${live}.bak"
runInstallOp cp "$tmp" "$live"
runCfgOp cp -a "$live" "${live}.bak"
{ [[ -n "$containers_dir" && "$live" == "$containers_dir"* ]] && runFileWrite "$live" < "$tmp" || runInstallOp cp "$tmp" "$live"; }
isSuccessful "Reconciled config: $(basename "$live") (backup: $(basename "$live").bak)"
fi
rm -f "$tmp"

View File

@ -27,13 +27,13 @@ changeUserGroupOnFolder()
checkSuccess "Updating $directory user to be $target_user... This may take a while..."
# Check if the source group exists
local source_group=$(id -g -n "$source_user")
local source_group; source_group=$(id -g -n "$source_user")
if [ $? -ne 0 ]; then
isError "Unable to determine source group for user '$source_user'."
fi
# Check if the target group exists
local target_group=$(id -g -n "$target_user")
local target_group; target_group=$(id -g -n "$target_user")
if [ $? -ne 0 ]; then
isError "Unable to determine target group for user '$target_user'."
fi

View File

@ -8,7 +8,7 @@ ipFindAvailable()
local start_last=2 # Hardcoded sensible default: .2
local end_last=254 # Hardcoded sensible default: .254
local existing_ips=$(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT resource_value FROM network_resources WHERE resource_type = 'ip' AND status = 'active';" 2>/dev/null)
local existing_ips; existing_ips=$(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT resource_value FROM network_resources WHERE resource_type = 'ip' AND status = 'active';" 2>/dev/null)
if [[ $? -ne 0 ]]; then
isError "Database query failed while checking existing IPs"
available_ip=""

View File

@ -84,7 +84,7 @@ peerPullApp()
# of the flow on a partial extract.
(
set -o pipefail
peerExec "$peer_name" "stream-app $app" | tar -C "$containers_dir" -xf -
peerExec "$peer_name" "stream-app $app" | runFileOp tar -C "$containers_dir" -xf -
)
local transfer_rc=$?
if (( transfer_rc != 0 )); then

View File

@ -71,25 +71,25 @@ HEALTH_REPORT_FILE="$TASK_DIR/health_report.json"
healthLogInfo() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "$timestamp [INFO] [HEALTH_CHECK] $message" | tee -a "$HEALTH_LOG_FILE"
echo "$timestamp [INFO] [HEALTH_CHECK] $message" | runFileWrite -a "$HEALTH_LOG_FILE"
}
healthLogError() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "$timestamp [ERROR] [HEALTH_CHECK] $message" | tee -a "$HEALTH_LOG_FILE"
echo "$timestamp [ERROR] [HEALTH_CHECK] $message" | runFileWrite -a "$HEALTH_LOG_FILE"
}
healthLogWarning() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "$timestamp [WARN] [HEALTH_CHECK] $message" | tee -a "$HEALTH_LOG_FILE"
echo "$timestamp [WARN] [HEALTH_CHECK] $message" | runFileWrite -a "$HEALTH_LOG_FILE"
}
healthLogSuccess() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "$timestamp [SUCCESS] [HEALTH_CHECK] $message" | tee -a "$HEALTH_LOG_FILE"
echo "$timestamp [SUCCESS] [HEALTH_CHECK] $message" | runFileWrite -a "$HEALTH_LOG_FILE"
}
# ========================================
@ -673,11 +673,11 @@ performMaintenance() {
healthLogInfo "Performing system maintenance..."
# Clean up old logs
find "$TASK_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
runFileOp find "$TASK_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
# Clean up temporary files
find "$TASK_DIR" -name "*.tmp" -delete 2>/dev/null
find "$TASK_DIR" -name ".queue.*" -delete 2>/dev/null
runFileOp find "$TASK_DIR" -name "*.tmp" -delete 2>/dev/null
runFileOp find "$TASK_DIR" -name ".queue.*" -delete 2>/dev/null
# Rotate large logs
for logFile in "$TASK_DIR"/*.log; do

View File

@ -164,7 +164,7 @@ EOF
if [[ -n "$icon_file" ]]; then
local icons_apps_dir="${containers_dir}libreportal/frontend/core/icons/apps"
runFileOp mkdir -p "$icons_apps_dir"
cp "$dir/$icon_file" "$icons_apps_dir/$icon_file" 2>/dev/null
runFileWrite "$icons_apps_dir/$icon_file" < "$dir/$icon_file" 2>/dev/null
fi
# Service names — one awk pass instead of 9 grep|awk|head pipelines.

View File

@ -16,16 +16,16 @@ createTaskFile() {
# Set task directory
local task_dir="${containers_dir}libreportal/frontend/data/tasks"
# Ensure task directory exists
# Ensure task directory exists (container-owned WebUI data tree -> runFileOp)
if [ ! -d "$task_dir" ]; then
mkdir -p "$task_dir"
chown -R $docker_install_user:$docker_install_user "$task_dir"
runFileOp mkdir -p "$task_dir"
runFileOp chown -R $docker_install_user:$docker_install_user "$task_dir"
fi
# Ensure queue.json exists
if [ ! -f "$task_dir/queue.json" ]; then
echo "[]" | tee "$task_dir/queue.json" >/dev/null
chown $docker_install_user:$docker_install_user "$task_dir/queue.json"
echo "[]" | runFileWrite "$task_dir/queue.json"
runFileOp chown $docker_install_user:$docker_install_user "$task_dir/queue.json"
fi
# Generate unique task ID
@ -55,26 +55,26 @@ createTaskFile() {
task_json+="}"
# Write task file
echo "$task_json" | tee "$task_file" >/dev/null
chown $docker_install_user:$docker_install_user "$task_file"
# Write task file (container-owned -> runFileWrite tees as the install user)
echo "$task_json" | runFileWrite "$task_file"
runFileOp chown $docker_install_user:$docker_install_user "$task_file"
# Add task ID to queue.json
local queue_content=$(cat "$task_dir/queue.json" 2>/dev/null)
if command -v jq >/dev/null 2>&1; then
# Use jq if available
echo "$queue_content" | jq --arg task "$task_id" '. + [$task]' | tee "$task_dir/queue.json" >/dev/null
echo "$queue_content" | jq --arg task "$task_id" '. + [$task]' | runFileWrite "$task_dir/queue.json"
else
# Fallback: manual JSON manipulation
if [ "$queue_content" = "[]" ]; then
echo "[\"$task_id\"]" | tee "$task_dir/queue.json" >/dev/null
echo "[\"$task_id\"]" | runFileWrite "$task_dir/queue.json"
else
echo "$queue_content" | sed 's/\]$/,\n"'$task_id'"]/' | tee "$task_dir/queue.json" >/dev/null
echo "$queue_content" | sed 's/\]$/,\n"'$task_id'"]/' | runFileWrite "$task_dir/queue.json"
fi
fi
chown $docker_install_user:$docker_install_user "$task_dir/queue.json"
runFileOp chown $docker_install_user:$docker_install_user "$task_dir/queue.json"
isSuccessful "Task created: $task_id"
}

View File

@ -232,7 +232,7 @@ webuiSystemAppStorage() {
if [[ -z "$ids" ]]; then
printf '{"apps":[],"total":0,"total_local":0,"total_external":0,"updated":"%s"}\n' "$now_iso" \
| runFileWrite "$final_file"
runAsManager touch "$stamp_file" 2>/dev/null || touch "$stamp_file" 2>/dev/null
runFileOp touch "$stamp_file" 2>/dev/null || true
return 0
fi
@ -297,7 +297,7 @@ webuiSystemAppStorage() {
runFileWrite "$final_file" < "$tmp"
fi
rm -f "$tmp"
runAsManager touch "$stamp_file" 2>/dev/null || touch "$stamp_file" 2>/dev/null
runFileOp touch "$stamp_file" 2>/dev/null || true
}
# Per-app Docker resource snapshot, grouped by compose project. Apps without a

View File

@ -120,7 +120,7 @@ EOF
local _lv; _lv=$(lpReleaseLatestVersion 2>/dev/null)
if [[ -n "$_lv" ]]; then
latest_version="$_lv"
runAsManager touch "$stamp_file" 2>/dev/null || touch "$stamp_file" 2>/dev/null
runFileOp touch "$stamp_file" 2>/dev/null || true
# Does the published release bump the root-owned footprint? If so,
# applying it needs a root re-install, not a WebUI `update apply`.
if declare -f lpReleaseLatestFootprint >/dev/null 2>&1; then
@ -192,7 +192,7 @@ EOF
fi
if [[ "$_fetched" == "true" ]]; then
runAsManager touch "$stamp_file" 2>/dev/null || touch "$stamp_file" 2>/dev/null
runFileOp touch "$stamp_file" 2>/dev/null || true
else
fetch_error="Could not reach the update server."
fi
@ -260,7 +260,7 @@ webuiSystemVerify() {
declare -f lpVerifyInstall >/dev/null 2>&1 || return 0
lpVerifyInstall
runAsManager touch "$stamp_file" 2>/dev/null || touch "$stamp_file" 2>/dev/null
runFileOp touch "$stamp_file" 2>/dev/null || true
# Encode the sample-paths array (each path JSON-escaped).
local sample_json="[]"

View File

@ -7,6 +7,6 @@ webuiRemoveSetupLock() {
local lock_file="$containers_dir/libreportal/frontend/data/setup.lock"
if [ -f "$lock_file" ]; then
rm "$lock_file"
runFileOp rm -f "$lock_file"
fi
}

View File

@ -14,7 +14,7 @@ webuiSyncAppIcon() {
[[ -z "$app_name" ]] && return 1
local icons_dir="${containers_dir}libreportal/frontend/core/icons/apps"
mkdir -p "$icons_dir"
runFileOp mkdir -p "$icons_dir"
# Icons live in the install template dir — copy_build_context.sh
# excludes *.svg from the install→deployed copy, so the deployed
@ -27,8 +27,8 @@ webuiSyncAppIcon() {
# cp to a temp name then mv so the webserver never serves a
# half-written file.
local dest="${icons_dir}/${app_name}.${ext}"
if cp "$src" "${dest}.tmp.$$" 2>/dev/null; then
mv "${dest}.tmp.$$" "$dest" 2>/dev/null
if runFileWrite "${dest}.tmp.$$" < "$src" 2>/dev/null; then
runFileOp mv "${dest}.tmp.$$" "$dest" 2>/dev/null
fi
return 0
fi