Boundaries that were supposed to hold
The security pass. Six findings, none reachable by visiting the site and every one a boundary this codebase says it keeps. A subagent is pinned to a list of read-only commands, in every mode, unattended, with no card anybody could approve -- and `find *` was on it. find writes files with -fprintf, runs programs with -exec and removes them with -delete, and none of that needs a character the metacharacter guard refuses. A page the model had just read could ask for a helper and get a key into authorized_keys, from Plan mode, which promises to change nothing. Refused in `subject()` rather than trimmed from the list: a pattern cannot say "and no dangerous flags", and "this one looks read-only" is exactly what put find there. The loopback guard missed `0.0.0.0`, which is not is_loopback but does connect to localhost -- so it answered a *decided* False and skipped the DNS half too. The one spelling of "this machine" that walked past a guard whose whole job is that sentence. Twice in the update helper, which is the one place this deliberately crosses a privilege boundary: root ran a script the service account owns, and root sourced a file that account can replace. Either turns a compromise of the web application into root. The first needed no compromise at all -- a pull happens as the service user and root runs whatever it fetched, so control of the branch was control of root. The old test asserted that exact ExecStart line and had pinned it in place. Push endpoints skipped check_url, the only outbound request that did. And a chat could be filed in another account's folder, which hands over its system prompt -- `_new_chat` resolved the folder, discarded it when it was not the caller's, and stored the raw id anyway. An existing helper install keeps the old wiring until install.sh is re-run; update.sh now says so when it finds itself inside the checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+44
-2
@@ -98,6 +98,39 @@ sudo -u "$SERVICE_USER" "$VENV/bin/pip" install --quiet -e "$APP[$LEMBAS_EXTRAS]
|
||||
# The drift is worth catching: a change in the unit can be what makes a release
|
||||
# work at all, and a host that pulled the code without it would run the new
|
||||
# version under the old settings and fail confusingly.
|
||||
# This script itself, first, because root is running a copy of it.
|
||||
#
|
||||
# `install.sh` puts a root-owned copy outside the checkout and points the unit
|
||||
# there -- root must not execute a file the unprivileged service account can
|
||||
# write, nor one that an update just fetched. The cost of that is exactly this:
|
||||
# the copy can fall behind what the checkout ships, silently, and the way to
|
||||
# notice is to compare.
|
||||
#
|
||||
# `$0` is the copy being run; `$APP/deploy/update.sh` is what was just pulled.
|
||||
self=$(readlink -f "$0")
|
||||
if [[ "$self" == "$(readlink -f "$APP")"/* ]]; then
|
||||
# The old wiring, and the one that matters: the unit points *into the
|
||||
# checkout*, so root is executing a file the unprivileged service account
|
||||
# owns and that every update overwrites. Fires on exactly the hosts installed
|
||||
# before this was fixed, and never afterwards.
|
||||
echo "== update helper: INSECURE WIRING ==" >&2
|
||||
echo " This unit runs $self as root, and that file is owned by" >&2
|
||||
echo " $SERVICE_USER -- the account the web application runs as. Anything" >&2
|
||||
echo " able to write as that account can rewrite it and be root, and so" >&2
|
||||
echo " can whoever controls the branch this host follows." >&2
|
||||
echo " Fix by re-running the installer, which moves root's copy out:" >&2
|
||||
echo " cd $APP && sudo INSTALL_UPDATE_HELPER=1 ./deploy/install.sh" >&2
|
||||
elif [[ -f "$APP/deploy/update.sh" ]]; then
|
||||
running_helper=$(sha256sum "$self" | cut -d' ' -f1)
|
||||
shipped_helper=$(sha256sum "$APP/deploy/update.sh" | cut -d' ' -f1)
|
||||
if [[ "$running_helper" != "$shipped_helper" ]]; then
|
||||
echo "== update helper ==" >&2
|
||||
echo " deploy/update.sh has changed since this host's copy was installed." >&2
|
||||
echo " Re-run the installer to take it:" >&2
|
||||
echo " cd $APP && sudo INSTALL_UPDATE_HELPER=1 ./deploy/install.sh" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
STAMP="$PREFIX/.unit-applied"
|
||||
current=$(sha256sum "$APP/deploy/lembas.service" | cut -d' ' -f1)
|
||||
if [[ -f "$STAMP" && "$(cat "$STAMP")" != "$current" ]]; then
|
||||
@@ -130,9 +163,18 @@ site_host=""; app_port=""
|
||||
# recovered from the environment file and the vhost found by what it proxies to.
|
||||
# Guessing "your-host" instead would have skipped the check on exactly the hosts
|
||||
# it was added for.
|
||||
# **Parsed, never sourced.** `.deploy-env` is written by the installer with
|
||||
# `sudo tee`, so the file is root-owned -- but `$PREFIX` is the service
|
||||
# account's own directory, mode 755, and write permission on a directory is all
|
||||
# it takes to unlink a file and put another one there. `.` would have run its
|
||||
# contents as root, and this script is root-triggerable by anyone who can create
|
||||
# one file in `$PREFIX/data` -- which is that same account. Two keys, two
|
||||
# patterns, and anything else in the file is ignored rather than executed.
|
||||
if [[ -f "$PREFIX/.deploy-env" ]]; then
|
||||
. "$PREFIX/.deploy-env"
|
||||
site_host="$SITE_HOST"; app_port="$APP_PORT"
|
||||
site_host=$(sed -n 's/^SITE_HOST=\([A-Za-z0-9._-]\{1,253\}\)$/\1/p' \
|
||||
"$PREFIX/.deploy-env" | tail -1)
|
||||
app_port=$(sed -n 's/^APP_PORT=\([0-9]\{1,5\}\)$/\1/p' \
|
||||
"$PREFIX/.deploy-env" | tail -1)
|
||||
fi
|
||||
if [[ -z "$app_port" && -f "$PREFIX/lembas.env" ]]; then
|
||||
app_port=$(sed -n 's/^LEMBAS_PORT=//p' "$PREFIX/lembas.env" | tail -1)
|
||||
|
||||
Reference in New Issue
Block a user