From 64587679a4476f5f9b7de66fba4361507182c368 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 29 Sep 2026 10:56:53 +0000 Subject: [PATCH] Entrypoint checks DATA_DIR in full before acting on it as root (closes #80) `bin/entrypoint.sh` now checks `DATA_DIR` before it creates anything or changes an owner or mode: it must be `/data` or a path below it with no `.`, `..` or empty part, and no part of it that exists, `/data` included, may be a symbolic link. Anything else stops the start with one message naming `DATA_DIR`. Only then does it create `DATA_DIR`, give `/data` and everything in it to `netwatch` (`chown -R -h`, so a link in it is not followed) and set mode 750 on `/data` and `DATA_DIR`. The README section "Running under upaas" says which values are accepted. Model: opus-5-5 --- README.md | 6 ++++-- TODO.md | 8 ++++++++ bin/entrypoint.sh | 48 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index acbb62d..248651d 100644 --- a/README.md +++ b/README.md @@ -216,8 +216,10 @@ What the [upaas](https://git.eeqj.de/sneak/upaas) app for netwatch needs: - `CORS_ALLOWED_ORIGINS`, default empty: other origins whose pages may call the API - `DEBUG`, default `false`: debug logging - - `DATA_DIR`, default `/data/reports`: leave unset; reports kept outside - `/data` do not survive a redeploy + - `DATA_DIR`, default `/data/reports`: the directory the reports are kept + in: `/data` or a path below it, with no `.` or `..` part and no extra `/`. + The container also stops if a part of the path that exists, `/data` + included, is a symbolic link - `TRUSTED_PROXIES`, default empty: set it to the address the reverse proxy in front of the container connects from, as an IP address or CIDR; several are separated by commas. nginx takes the client address from diff --git a/TODO.md b/TODO.md index 8b17dd2..505c728 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,14 @@ latest run passes. # Completed Steps +- 2026-09-29: `bin/entrypoint.sh` checks `DATA_DIR` in full before it acts on it + as root (issue #80): `DATA_DIR` must be `/data` or a path below it with no + `.`, `..` or empty part, and no part of it that exists, `/data` included, may + be a symbolic link; anything else stops the start with a message naming + `DATA_DIR`. Only then is `DATA_DIR` created and `/data` given to `netwatch`, + so a refused start no longer creates directories outside `/data`, and + `DATA_DIR=/etc` no longer gives `/etc` to `netwatch`. The `README.md` section + "Running under upaas" says which values are accepted - 2026-09-29: the container sets up its own data directory (issue #75): `bin/entrypoint.sh`, still as root, creates `DATA_DIR` if missing and gives it and `/data` to the `netwatch` user with mode 750 before starting the backend diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh index 6f3bdf8..323ca1d 100755 --- a/bin/entrypoint.sh +++ b/bin/entrypoint.sh @@ -63,25 +63,43 @@ done > /etc/nginx/trusted-proxies.conf # netwatch-server keeps its report files in DATA_DIR, on the /data # volume, which may be a host directory owned by root or by another -# uid. Both are given to the netwatch user here, with the mode the -# server gives a directory it creates, so the host directory needs no -# preparing. +# uid. /data and everything in it are given to the netwatch user here, +# and /data and DATA_DIR get the mode the server gives a directory it +# creates, so the host directory needs no preparing. # -# chown and chmod, run as root, change whatever a symbolic link on the -# path points to, anywhere in the container, and the netwatch user can -# put one in /data. So the start stops unless readlink -f, which -# follows every link on a path, gives /data and DATA_DIR back as they -# are. It also writes a path in full, so a DATA_DIR with '.', '..' or -# an extra '/' in it is refused too. +# This runs as root, so nothing is created or changed until DATA_DIR is +# known to be /data or a path below it, with no '.', '..' or empty +# part, and no part of it that exists, /data included, is a symbolic +# link: the netwatch user can put one in /data, and root would follow +# it anywhere in the container. Nothing else runs in the container yet, +# so no link can appear after the check. export DATA_DIR="${DATA_DIR:-/data/reports}" -mkdir -p "$DATA_DIR" || exit 1 -if [ "$(readlink -f /data)" != /data ] || - [ "$(readlink -f "$DATA_DIR")" != "$DATA_DIR" ]; then - echo "entrypoint: DATA_DIR must be a full path with no '.', '..'," \ - "extra '/' or symbolic link on it or on /data, not '$DATA_DIR'" >&2 +data_dir_ok() { + # With a / added at the end, a last part of '.' or '..', and a / at + # the end, match these patterns too. + case "$DATA_DIR/" in + */./* | */../* | *//*) return 1 ;; + /data/*) ;; + *) return 1 ;; + esac + # Each part from DATA_DIR up to /data. [ -L ] is false for a part + # that does not exist. + dir="$DATA_DIR" + while [ "$dir" != /data ]; do + [ -L "$dir" ] && return 1 + dir="${dir%/*}" + done + [ ! -L /data ] +} +if ! data_dir_ok; then + echo "entrypoint: DATA_DIR must be /data or a path below it, with no" \ + "'.', '..', extra '/' or symbolic link on it, not '$DATA_DIR'" >&2 exit 1 fi -chown -R netwatch:netwatch /data "$DATA_DIR" || exit 1 +mkdir -p "$DATA_DIR" || exit 1 +# -h: a symbolic link in /data is itself given to netwatch, not what it +# points to. +chown -R -h netwatch:netwatch /data || exit 1 chmod 750 /data "$DATA_DIR" || exit 1 # A stop signal is only noted here; the loop below acts on it. -- 2.54.0